canvas-parent-mcp 1.2.4 → 1.2.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/bundle.js +532 -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 Canvas LMS (Instructure) — courses, grades, assignments, announcements, planner, conversations, files",
|
|
10
|
-
"version": "1.2.
|
|
10
|
+
"version": "1.2.5"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Canvas LMS",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Canvas LMS (Instructure) MCP server for Claude — student/observer access via natural language",
|
|
18
|
-
"version": "1.2.
|
|
18
|
+
"version": "1.2.5",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-parent-mcp",
|
|
3
3
|
"displayName": "Canvas LMS",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.5",
|
|
5
5
|
"description": "Canvas LMS (Instructure) MCP server for Claude — courses, grades, assignments, announcements, planner, conversations, files",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
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 resolved2 = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
|
|
3655
3660
|
schemelessOptions.skipEscape = true;
|
|
3656
3661
|
return serialize(resolved2, 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
|
|
@@ -34998,8 +35018,15 @@ function createOAuth2Refresher(opts) {
|
|
|
34998
35018
|
}
|
|
34999
35019
|
|
|
35000
35020
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
35001
|
-
var PROTOCOL_VERSION =
|
|
35021
|
+
var PROTOCOL_VERSION = 3;
|
|
35002
35022
|
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
35023
|
+
function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
|
|
35024
|
+
const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
|
|
35025
|
+
out.set(mcpHelloNonce, 0);
|
|
35026
|
+
out.set(extHelloNonce, mcpHelloNonce.length);
|
|
35027
|
+
out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
|
|
35028
|
+
return out;
|
|
35029
|
+
}
|
|
35003
35030
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
35004
35031
|
"fetch",
|
|
35005
35032
|
"read_cookies",
|
|
@@ -35010,7 +35037,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
35010
35037
|
"read_indexed_db",
|
|
35011
35038
|
"read_dom",
|
|
35012
35039
|
"download",
|
|
35013
|
-
"graphql"
|
|
35040
|
+
"graphql",
|
|
35041
|
+
"write_cookies"
|
|
35014
35042
|
]);
|
|
35015
35043
|
|
|
35016
35044
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35121,6 +35149,15 @@ function assertHttpUrl(x, label) {
|
|
|
35121
35149
|
throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
|
|
35122
35150
|
}
|
|
35123
35151
|
}
|
|
35152
|
+
function assertCookiePath(x, label) {
|
|
35153
|
+
assertString(x, label);
|
|
35154
|
+
if (!x.startsWith("/") || x.startsWith("//")) {
|
|
35155
|
+
throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
|
|
35156
|
+
}
|
|
35157
|
+
if (x.includes("?") || x.includes("#") || x.includes("\\")) {
|
|
35158
|
+
throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
|
|
35159
|
+
}
|
|
35160
|
+
}
|
|
35124
35161
|
function assertHttpsOriginOnly(x, label) {
|
|
35125
35162
|
assertString(x, label);
|
|
35126
35163
|
let u;
|
|
@@ -35565,13 +35602,43 @@ function validateInnerRequest(raw) {
|
|
|
35565
35602
|
}
|
|
35566
35603
|
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35567
35604
|
assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
|
|
35605
|
+
if (raw.init.path !== void 0)
|
|
35606
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35568
35607
|
for (const k of Object.keys(raw.init)) {
|
|
35569
|
-
if (k !== "origin" && k !== "keys") {
|
|
35608
|
+
if (k !== "origin" && k !== "keys" && k !== "path") {
|
|
35570
35609
|
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
|
|
35571
35610
|
}
|
|
35572
35611
|
}
|
|
35573
35612
|
return raw;
|
|
35574
35613
|
}
|
|
35614
|
+
if (raw.op === "write_cookies") {
|
|
35615
|
+
assertObject(raw.init, "inner.init");
|
|
35616
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35617
|
+
if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
|
|
35618
|
+
throw new ProtocolError("inner.init.cookies: must be a non-empty array");
|
|
35619
|
+
}
|
|
35620
|
+
for (const [i, entry] of raw.init.cookies.entries()) {
|
|
35621
|
+
assertObject(entry, `inner.init.cookies[${i}]`);
|
|
35622
|
+
assertString(entry.name, `inner.init.cookies[${i}].name`);
|
|
35623
|
+
if (!SCOPE_KEY_RE.test(entry.name)) {
|
|
35624
|
+
throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
|
|
35625
|
+
}
|
|
35626
|
+
assertString(entry.value, `inner.init.cookies[${i}].value`);
|
|
35627
|
+
for (const k of Object.keys(entry)) {
|
|
35628
|
+
if (k !== "name" && k !== "value") {
|
|
35629
|
+
throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35630
|
+
}
|
|
35631
|
+
}
|
|
35632
|
+
}
|
|
35633
|
+
if (raw.init.path !== void 0)
|
|
35634
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35635
|
+
for (const k of Object.keys(raw.init)) {
|
|
35636
|
+
if (k !== "origin" && k !== "cookies" && k !== "path") {
|
|
35637
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
|
|
35638
|
+
}
|
|
35639
|
+
}
|
|
35640
|
+
return raw;
|
|
35641
|
+
}
|
|
35575
35642
|
if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
|
|
35576
35643
|
assertObject(raw.init, "inner.init");
|
|
35577
35644
|
if (raw.init.origin === void 0) {
|
|
@@ -35758,7 +35825,7 @@ function validateInnerRequest(raw) {
|
|
|
35758
35825
|
}
|
|
35759
35826
|
return raw;
|
|
35760
35827
|
}
|
|
35761
|
-
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)}`);
|
|
35828
|
+
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)}`);
|
|
35762
35829
|
}
|
|
35763
35830
|
function assertNonEmptyKeyArray(value, label) {
|
|
35764
35831
|
if (!Array.isArray(value)) {
|
|
@@ -35819,6 +35886,18 @@ function validateInnerResponse(raw) {
|
|
|
35819
35886
|
}
|
|
35820
35887
|
return raw;
|
|
35821
35888
|
}
|
|
35889
|
+
if (op === "write_cookies") {
|
|
35890
|
+
if (raw.written === void 0) {
|
|
35891
|
+
throw new ProtocolError("inner.written: missing on write_cookies response");
|
|
35892
|
+
}
|
|
35893
|
+
if (!Array.isArray(raw.written)) {
|
|
35894
|
+
throw new ProtocolError("inner.written: must be an array");
|
|
35895
|
+
}
|
|
35896
|
+
for (const [i, name] of raw.written.entries()) {
|
|
35897
|
+
assertString(name, `inner.written[${i}]`);
|
|
35898
|
+
}
|
|
35899
|
+
return raw;
|
|
35900
|
+
}
|
|
35822
35901
|
if (op === "read_local_storage" || op === "read_session_storage") {
|
|
35823
35902
|
if (raw.values === void 0) {
|
|
35824
35903
|
throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
|
|
@@ -36296,6 +36375,148 @@ async function awaitSessionReady(ready, opts) {
|
|
|
36296
36375
|
}
|
|
36297
36376
|
}
|
|
36298
36377
|
|
|
36378
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36379
|
+
import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
36380
|
+
import { join as join2 } from "node:path";
|
|
36381
|
+
|
|
36382
|
+
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36383
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36384
|
+
import { join } from "node:path";
|
|
36385
|
+
import { homedir } from "node:os";
|
|
36386
|
+
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36387
|
+
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36388
|
+
function defaultIdentityDir() {
|
|
36389
|
+
return join(homedir(), ".fetchproxy", "identity");
|
|
36390
|
+
}
|
|
36391
|
+
function safeIdentityFileBase(serverName) {
|
|
36392
|
+
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36393
|
+
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36394
|
+
}
|
|
36395
|
+
return serverName.replace(/\//g, "_");
|
|
36396
|
+
}
|
|
36397
|
+
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36398
|
+
const safeFile = safeIdentityFileBase(serverName);
|
|
36399
|
+
const path = join(dir, `${safeFile}.json`);
|
|
36400
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36401
|
+
try {
|
|
36402
|
+
const raw = await readFile(path, "utf8");
|
|
36403
|
+
const j2 = JSON.parse(raw);
|
|
36404
|
+
return {
|
|
36405
|
+
x25519Priv: fromB64(j2.x25519Priv),
|
|
36406
|
+
x25519Pub: fromB64(j2.x25519Pub),
|
|
36407
|
+
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36408
|
+
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36409
|
+
createdAt: j2.createdAt
|
|
36410
|
+
};
|
|
36411
|
+
} catch (e) {
|
|
36412
|
+
if (e.code !== "ENOENT")
|
|
36413
|
+
throw e;
|
|
36414
|
+
}
|
|
36415
|
+
const x = await generateX25519();
|
|
36416
|
+
const ed = await generateEd25519();
|
|
36417
|
+
const id = {
|
|
36418
|
+
x25519Priv: x.privateKey,
|
|
36419
|
+
x25519Pub: x.publicKey,
|
|
36420
|
+
ed25519Priv: ed.privateKey,
|
|
36421
|
+
ed25519Pub: ed.publicKey,
|
|
36422
|
+
createdAt: Date.now()
|
|
36423
|
+
};
|
|
36424
|
+
const j = {
|
|
36425
|
+
x25519Priv: toB64(id.x25519Priv),
|
|
36426
|
+
x25519Pub: toB64(id.x25519Pub),
|
|
36427
|
+
ed25519Priv: toB64(id.ed25519Priv),
|
|
36428
|
+
ed25519Pub: toB64(id.ed25519Pub),
|
|
36429
|
+
createdAt: id.createdAt
|
|
36430
|
+
};
|
|
36431
|
+
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36432
|
+
await chmod(path, 384);
|
|
36433
|
+
return id;
|
|
36434
|
+
}
|
|
36435
|
+
|
|
36436
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36437
|
+
function fileExtensionTrust(args) {
|
|
36438
|
+
return {
|
|
36439
|
+
allowNew: args.allowNew,
|
|
36440
|
+
location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36441
|
+
read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36442
|
+
write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
|
|
36443
|
+
};
|
|
36444
|
+
}
|
|
36445
|
+
var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
|
|
36446
|
+
function allowNewExtensionIdentity(explicit, env = process.env) {
|
|
36447
|
+
if (explicit !== void 0)
|
|
36448
|
+
return explicit;
|
|
36449
|
+
return env[TRUST_NEW_EXTENSION_ENV] === "1";
|
|
36450
|
+
}
|
|
36451
|
+
function decideExtensionTrust(args) {
|
|
36452
|
+
const { pin, hello, allowNew, serverName } = args;
|
|
36453
|
+
if (!pin)
|
|
36454
|
+
return { decision: "first-use" };
|
|
36455
|
+
if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
|
|
36456
|
+
return { decision: "pinned" };
|
|
36457
|
+
}
|
|
36458
|
+
const trustPath = args.location ?? extensionTrustPathHint(serverName);
|
|
36459
|
+
if (allowNew) {
|
|
36460
|
+
return {
|
|
36461
|
+
decision: "replace",
|
|
36462
|
+
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.`
|
|
36463
|
+
};
|
|
36464
|
+
}
|
|
36465
|
+
return {
|
|
36466
|
+
decision: "refused",
|
|
36467
|
+
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.`
|
|
36468
|
+
};
|
|
36469
|
+
}
|
|
36470
|
+
function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
|
|
36471
|
+
return join2(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
|
|
36472
|
+
}
|
|
36473
|
+
function extensionTrustPathHint(serverName) {
|
|
36474
|
+
try {
|
|
36475
|
+
return extensionTrustPath(serverName);
|
|
36476
|
+
} catch {
|
|
36477
|
+
return join2(defaultIdentityDir(), "<server-name>.extension-trust.json");
|
|
36478
|
+
}
|
|
36479
|
+
}
|
|
36480
|
+
function isPin(x) {
|
|
36481
|
+
if (!x || typeof x !== "object")
|
|
36482
|
+
return false;
|
|
36483
|
+
const r = x;
|
|
36484
|
+
return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
|
|
36485
|
+
}
|
|
36486
|
+
async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
|
|
36487
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36488
|
+
let raw;
|
|
36489
|
+
try {
|
|
36490
|
+
raw = await readFile2(path, "utf8");
|
|
36491
|
+
} catch (e) {
|
|
36492
|
+
if (e.code === "ENOENT")
|
|
36493
|
+
return null;
|
|
36494
|
+
throw e;
|
|
36495
|
+
}
|
|
36496
|
+
let parsed;
|
|
36497
|
+
try {
|
|
36498
|
+
parsed = JSON.parse(raw);
|
|
36499
|
+
} catch {
|
|
36500
|
+
throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
|
|
36501
|
+
}
|
|
36502
|
+
if (!isPin(parsed)) {
|
|
36503
|
+
throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
|
|
36504
|
+
}
|
|
36505
|
+
return {
|
|
36506
|
+
identityX25519Pub: parsed.identityX25519Pub,
|
|
36507
|
+
identityEd25519Pub: parsed.identityEd25519Pub,
|
|
36508
|
+
pinnedAt: parsed.pinnedAt
|
|
36509
|
+
};
|
|
36510
|
+
}
|
|
36511
|
+
async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
|
|
36512
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36513
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
36514
|
+
const tmp = `${path}.tmp`;
|
|
36515
|
+
await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
|
|
36516
|
+
await chmod2(tmp, 384);
|
|
36517
|
+
await rename(tmp, path);
|
|
36518
|
+
}
|
|
36519
|
+
|
|
36299
36520
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
36300
36521
|
var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
36301
36522
|
var enc2 = new TextEncoder();
|
|
@@ -36349,9 +36570,12 @@ async function startHost(opts) {
|
|
|
36349
36570
|
}
|
|
36350
36571
|
resetSessionPromise();
|
|
36351
36572
|
let extensionHello = null;
|
|
36573
|
+
let extensionClaim = null;
|
|
36352
36574
|
wss.on("connection", (ws) => {
|
|
36353
36575
|
let identified = null;
|
|
36354
36576
|
let peerMcpId = null;
|
|
36577
|
+
let closed = false;
|
|
36578
|
+
let pinOnReady = false;
|
|
36355
36579
|
ws.on("message", async (data) => {
|
|
36356
36580
|
try {
|
|
36357
36581
|
let frame;
|
|
@@ -36363,10 +36587,43 @@ async function startHost(opts) {
|
|
|
36363
36587
|
return;
|
|
36364
36588
|
}
|
|
36365
36589
|
if (frame.type === "hello" && frame.role === "extension") {
|
|
36366
|
-
if (extensionWs) {
|
|
36590
|
+
if (extensionWs || extensionClaim) {
|
|
36367
36591
|
ws.close(1008, "extension already connected");
|
|
36368
36592
|
return;
|
|
36369
36593
|
}
|
|
36594
|
+
extensionClaim = ws;
|
|
36595
|
+
let pin;
|
|
36596
|
+
try {
|
|
36597
|
+
pin = await opts.extensionTrust.read();
|
|
36598
|
+
} catch (e) {
|
|
36599
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36600
|
+
if (extensionClaim === ws)
|
|
36601
|
+
extensionClaim = null;
|
|
36602
|
+
ws.close(1008, "extension pin unreadable");
|
|
36603
|
+
return;
|
|
36604
|
+
}
|
|
36605
|
+
const outcome = decideExtensionTrust({
|
|
36606
|
+
pin,
|
|
36607
|
+
hello: frame,
|
|
36608
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36609
|
+
serverName: opts.ownServerName,
|
|
36610
|
+
location: opts.extensionTrust.location
|
|
36611
|
+
});
|
|
36612
|
+
if (outcome.decision === "refused") {
|
|
36613
|
+
console.warn(outcome.message);
|
|
36614
|
+
if (extensionClaim === ws)
|
|
36615
|
+
extensionClaim = null;
|
|
36616
|
+
ws.close(1008, "extension identity is not the pinned one");
|
|
36617
|
+
return;
|
|
36618
|
+
}
|
|
36619
|
+
if (outcome.decision === "replace")
|
|
36620
|
+
console.warn(outcome.message);
|
|
36621
|
+
if (closed || ws.readyState !== import_websocket.default.OPEN) {
|
|
36622
|
+
if (extensionClaim === ws)
|
|
36623
|
+
extensionClaim = null;
|
|
36624
|
+
return;
|
|
36625
|
+
}
|
|
36626
|
+
pinOnReady = outcome.decision !== "pinned";
|
|
36370
36627
|
identified = "extension";
|
|
36371
36628
|
extensionWs = ws;
|
|
36372
36629
|
extensionHello = frame;
|
|
@@ -36378,6 +36635,8 @@ async function startHost(opts) {
|
|
|
36378
36635
|
console.error("[fetchproxy] onPairCode threw:", e);
|
|
36379
36636
|
}
|
|
36380
36637
|
}
|
|
36638
|
+
for (const slot of peers.values())
|
|
36639
|
+
slot.ws.send(JSON.stringify(frame));
|
|
36381
36640
|
ws.send(JSON.stringify(ownHello));
|
|
36382
36641
|
for (const slot of peers.values()) {
|
|
36383
36642
|
ws.send(JSON.stringify(slot.helloFrame));
|
|
@@ -36413,6 +36672,8 @@ async function startHost(opts) {
|
|
|
36413
36672
|
peers.set(frame.mcpId, { ws, helloFrame: frame });
|
|
36414
36673
|
if (extensionWs)
|
|
36415
36674
|
extensionWs.send(JSON.stringify(frame));
|
|
36675
|
+
if (extensionHello)
|
|
36676
|
+
ws.send(JSON.stringify(extensionHello));
|
|
36416
36677
|
return;
|
|
36417
36678
|
}
|
|
36418
36679
|
if (frame.type === "ready") {
|
|
@@ -36424,7 +36685,7 @@ async function startHost(opts) {
|
|
|
36424
36685
|
}
|
|
36425
36686
|
const extEdPub = fromB64(extensionHello.identityEd25519Pub);
|
|
36426
36687
|
const extNonce = fromB64(extensionHello.sessionNonce);
|
|
36427
|
-
const msg =
|
|
36688
|
+
const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
|
|
36428
36689
|
const sig = fromB64(frame.sessionSig);
|
|
36429
36690
|
let sigOk = false;
|
|
36430
36691
|
try {
|
|
@@ -36437,6 +36698,18 @@ async function startHost(opts) {
|
|
|
36437
36698
|
ws.close(1008, "extension session signature invalid");
|
|
36438
36699
|
return;
|
|
36439
36700
|
}
|
|
36701
|
+
if (pinOnReady) {
|
|
36702
|
+
pinOnReady = false;
|
|
36703
|
+
try {
|
|
36704
|
+
await opts.extensionTrust.write({
|
|
36705
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36706
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36707
|
+
pinnedAt: Date.now()
|
|
36708
|
+
});
|
|
36709
|
+
} catch (e) {
|
|
36710
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36711
|
+
}
|
|
36712
|
+
}
|
|
36440
36713
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36441
36714
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
36442
36715
|
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36490,6 +36763,9 @@ async function startHost(opts) {
|
|
|
36490
36763
|
}
|
|
36491
36764
|
});
|
|
36492
36765
|
ws.on("close", () => {
|
|
36766
|
+
closed = true;
|
|
36767
|
+
if (extensionClaim === ws)
|
|
36768
|
+
extensionClaim = null;
|
|
36493
36769
|
if (identified === "extension" && extensionWs === ws) {
|
|
36494
36770
|
extensionWs = null;
|
|
36495
36771
|
extensionHello = null;
|
|
@@ -36580,11 +36856,84 @@ async function startPeer(opts) {
|
|
|
36580
36856
|
resolveFirstReady = resolve;
|
|
36581
36857
|
rejectFirstReady = reject;
|
|
36582
36858
|
});
|
|
36859
|
+
let extensionHello = null;
|
|
36860
|
+
let warnedUnverifiable = false;
|
|
36861
|
+
let cachedPin = void 0;
|
|
36862
|
+
const authenticateExtension = async (sessionSig, extensionSessionPub) => {
|
|
36863
|
+
if (!extensionHello) {
|
|
36864
|
+
if (opts.requireExtensionIdentity) {
|
|
36865
|
+
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.`);
|
|
36866
|
+
return false;
|
|
36867
|
+
}
|
|
36868
|
+
if (!warnedUnverifiable) {
|
|
36869
|
+
warnedUnverifiable = true;
|
|
36870
|
+
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.`);
|
|
36871
|
+
}
|
|
36872
|
+
return true;
|
|
36873
|
+
}
|
|
36874
|
+
const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
|
|
36875
|
+
let sigOk = false;
|
|
36876
|
+
try {
|
|
36877
|
+
sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
|
|
36878
|
+
} catch {
|
|
36879
|
+
sigOk = false;
|
|
36880
|
+
}
|
|
36881
|
+
if (!sigOk) {
|
|
36882
|
+
console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
|
|
36883
|
+
return false;
|
|
36884
|
+
}
|
|
36885
|
+
if (cachedPin === void 0) {
|
|
36886
|
+
try {
|
|
36887
|
+
cachedPin = await opts.extensionTrust.read();
|
|
36888
|
+
} catch (e) {
|
|
36889
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36890
|
+
return false;
|
|
36891
|
+
}
|
|
36892
|
+
}
|
|
36893
|
+
const pin = cachedPin;
|
|
36894
|
+
const outcome = decideExtensionTrust({
|
|
36895
|
+
pin,
|
|
36896
|
+
hello: extensionHello,
|
|
36897
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36898
|
+
serverName: opts.serverName,
|
|
36899
|
+
location: opts.extensionTrust.location
|
|
36900
|
+
});
|
|
36901
|
+
if (outcome.decision === "refused") {
|
|
36902
|
+
console.warn(outcome.message);
|
|
36903
|
+
return false;
|
|
36904
|
+
}
|
|
36905
|
+
if (outcome.decision === "replace")
|
|
36906
|
+
console.warn(outcome.message);
|
|
36907
|
+
if (outcome.decision !== "pinned") {
|
|
36908
|
+
try {
|
|
36909
|
+
const written = {
|
|
36910
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36911
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36912
|
+
pinnedAt: Date.now()
|
|
36913
|
+
};
|
|
36914
|
+
await opts.extensionTrust.write(written);
|
|
36915
|
+
cachedPin = written;
|
|
36916
|
+
} catch (e) {
|
|
36917
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36918
|
+
}
|
|
36919
|
+
}
|
|
36920
|
+
return true;
|
|
36921
|
+
};
|
|
36583
36922
|
const onMessage = async (data) => {
|
|
36584
36923
|
try {
|
|
36585
36924
|
const raw = JSON.parse(data.toString());
|
|
36586
36925
|
const frame = validateFrame(raw);
|
|
36926
|
+
if (frame.type === "hello" && frame.role === "extension") {
|
|
36927
|
+
extensionHello = frame;
|
|
36928
|
+
return;
|
|
36929
|
+
}
|
|
36587
36930
|
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
36931
|
+
const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
|
|
36932
|
+
if (!authorised) {
|
|
36933
|
+
ws.close(1008, "extension identity refused");
|
|
36934
|
+
rejectFirstReady(new Error("peer: extension identity refused"));
|
|
36935
|
+
return;
|
|
36936
|
+
}
|
|
36588
36937
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36589
36938
|
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
36590
36939
|
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36665,57 +37014,6 @@ async function startPeer(opts) {
|
|
|
36665
37014
|
return handle;
|
|
36666
37015
|
}
|
|
36667
37016
|
|
|
36668
|
-
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36669
|
-
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36670
|
-
import { join } from "node:path";
|
|
36671
|
-
import { homedir } from "node:os";
|
|
36672
|
-
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36673
|
-
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36674
|
-
function defaultIdentityDir() {
|
|
36675
|
-
return join(homedir(), ".fetchproxy", "identity");
|
|
36676
|
-
}
|
|
36677
|
-
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36678
|
-
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36679
|
-
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36680
|
-
}
|
|
36681
|
-
const safeFile = serverName.replace(/\//g, "_");
|
|
36682
|
-
const path = join(dir, `${safeFile}.json`);
|
|
36683
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36684
|
-
try {
|
|
36685
|
-
const raw = await readFile(path, "utf8");
|
|
36686
|
-
const j2 = JSON.parse(raw);
|
|
36687
|
-
return {
|
|
36688
|
-
x25519Priv: fromB64(j2.x25519Priv),
|
|
36689
|
-
x25519Pub: fromB64(j2.x25519Pub),
|
|
36690
|
-
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36691
|
-
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36692
|
-
createdAt: j2.createdAt
|
|
36693
|
-
};
|
|
36694
|
-
} catch (e) {
|
|
36695
|
-
if (e.code !== "ENOENT")
|
|
36696
|
-
throw e;
|
|
36697
|
-
}
|
|
36698
|
-
const x = await generateX25519();
|
|
36699
|
-
const ed = await generateEd25519();
|
|
36700
|
-
const id = {
|
|
36701
|
-
x25519Priv: x.privateKey,
|
|
36702
|
-
x25519Pub: x.publicKey,
|
|
36703
|
-
ed25519Priv: ed.privateKey,
|
|
36704
|
-
ed25519Pub: ed.publicKey,
|
|
36705
|
-
createdAt: Date.now()
|
|
36706
|
-
};
|
|
36707
|
-
const j = {
|
|
36708
|
-
x25519Priv: toB64(id.x25519Priv),
|
|
36709
|
-
x25519Pub: toB64(id.x25519Pub),
|
|
36710
|
-
ed25519Priv: toB64(id.ed25519Priv),
|
|
36711
|
-
ed25519Pub: toB64(id.ed25519Pub),
|
|
36712
|
-
createdAt: id.createdAt
|
|
36713
|
-
};
|
|
36714
|
-
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36715
|
-
await chmod(path, 384);
|
|
36716
|
-
return id;
|
|
36717
|
-
}
|
|
36718
|
-
|
|
36719
37017
|
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
36720
37018
|
function classifyFetchError(error51) {
|
|
36721
37019
|
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
@@ -36797,6 +37095,39 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
|
36797
37095
|
this.hint = hint;
|
|
36798
37096
|
}
|
|
36799
37097
|
};
|
|
37098
|
+
var FetchproxyHintedError = class extends FetchproxyProtocolError {
|
|
37099
|
+
/** The extension's raw rejection, unmodified. */
|
|
37100
|
+
originalError;
|
|
37101
|
+
/** What the user should actually do, in prose. */
|
|
37102
|
+
hint;
|
|
37103
|
+
constructor(originalError, hint) {
|
|
37104
|
+
super(`${originalError} \u2014 ${hint}`);
|
|
37105
|
+
this.name = "FetchproxyHintedError";
|
|
37106
|
+
this.originalError = originalError;
|
|
37107
|
+
this.hint = hint;
|
|
37108
|
+
}
|
|
37109
|
+
};
|
|
37110
|
+
var FetchproxyScopeError = class extends FetchproxyHintedError {
|
|
37111
|
+
constructor(originalError) {
|
|
37112
|
+
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.");
|
|
37113
|
+
this.name = "FetchproxyScopeError";
|
|
37114
|
+
}
|
|
37115
|
+
};
|
|
37116
|
+
var FetchproxyNoTabError = class extends FetchproxyHintedError {
|
|
37117
|
+
constructor(originalError) {
|
|
37118
|
+
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.");
|
|
37119
|
+
this.name = "FetchproxyNoTabError";
|
|
37120
|
+
}
|
|
37121
|
+
};
|
|
37122
|
+
var SCOPE_REJECTION = /not in declared/;
|
|
37123
|
+
var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
|
|
37124
|
+
function protocolErrorFrom(error51) {
|
|
37125
|
+
if (SCOPE_REJECTION.test(error51))
|
|
37126
|
+
return new FetchproxyScopeError(error51);
|
|
37127
|
+
if (NO_TAB_REJECTION.test(error51))
|
|
37128
|
+
return new FetchproxyNoTabError(error51);
|
|
37129
|
+
return new FetchproxyProtocolError(error51);
|
|
37130
|
+
}
|
|
36800
37131
|
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
36801
37132
|
url;
|
|
36802
37133
|
timeoutMs;
|
|
@@ -36826,6 +37157,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
36826
37157
|
}
|
|
36827
37158
|
};
|
|
36828
37159
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
37160
|
+
function normalizeCookiePath(path) {
|
|
37161
|
+
if (path === void 0 || path === "")
|
|
37162
|
+
return void 0;
|
|
37163
|
+
const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
37164
|
+
try {
|
|
37165
|
+
assertCookiePath(trimmed, "path");
|
|
37166
|
+
} catch (e) {
|
|
37167
|
+
throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
|
|
37168
|
+
}
|
|
37169
|
+
return trimmed;
|
|
37170
|
+
}
|
|
36829
37171
|
function assertSubdomainLabel(label) {
|
|
36830
37172
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
36831
37173
|
throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
|
|
@@ -36890,6 +37232,9 @@ var FetchproxyServer = class {
|
|
|
36890
37232
|
// them off from `pending` (fetch) and `pendingReadCookies` (legacy
|
|
36891
37233
|
// string-shape) so the response routing in `onInner` stays linear.
|
|
36892
37234
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
37235
|
+
// 1.12.0+: write-cookies awaiters resolve the list of names actually
|
|
37236
|
+
// written, so a caller can confirm rather than assume.
|
|
37237
|
+
pendingWriteCookies = /* @__PURE__ */ new Map();
|
|
36893
37238
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
36894
37239
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
36895
37240
|
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
@@ -37016,6 +37361,8 @@ var FetchproxyServer = class {
|
|
|
37016
37361
|
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
|
|
37017
37362
|
keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
|
|
37018
37363
|
identityDir: opts.identityDir,
|
|
37364
|
+
allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
|
|
37365
|
+
requireExtensionIdentity: opts.requireExtensionIdentity,
|
|
37019
37366
|
onPairCode: opts.onPairCode
|
|
37020
37367
|
};
|
|
37021
37368
|
}
|
|
@@ -37115,7 +37462,8 @@ var FetchproxyServer = class {
|
|
|
37115
37462
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37116
37463
|
ownDomSelectors: this.opts.domSelectors,
|
|
37117
37464
|
ownGraphqlOps: this.opts.graphqlOps,
|
|
37118
|
-
onPairCode: this.opts.onPairCode
|
|
37465
|
+
onPairCode: this.opts.onPairCode,
|
|
37466
|
+
extensionTrust: this.extensionTrust()
|
|
37119
37467
|
});
|
|
37120
37468
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
37121
37469
|
this.hostHandle.onExtensionDisconnect(() => {
|
|
@@ -37144,7 +37492,9 @@ var FetchproxyServer = class {
|
|
|
37144
37492
|
localStoragePointers: this.opts.localStoragePointers,
|
|
37145
37493
|
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37146
37494
|
domSelectors: this.opts.domSelectors,
|
|
37147
|
-
graphqlOps: this.opts.graphqlOps
|
|
37495
|
+
graphqlOps: this.opts.graphqlOps,
|
|
37496
|
+
extensionTrust: this.extensionTrust(),
|
|
37497
|
+
requireExtensionIdentity: this.opts.requireExtensionIdentity
|
|
37148
37498
|
});
|
|
37149
37499
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
37150
37500
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37283,6 +37633,23 @@ var FetchproxyServer = class {
|
|
|
37283
37633
|
markActive() {
|
|
37284
37634
|
this.noteActivityForKeepalive();
|
|
37285
37635
|
}
|
|
37636
|
+
/**
|
|
37637
|
+
* #208: this MCP's pin on the extension's identity, stored beside its own
|
|
37638
|
+
* identity key and so following `identityDir` wherever the caller put it.
|
|
37639
|
+
*
|
|
37640
|
+
* `allowNewExtensionIdentity` falls back to an environment variable when the
|
|
37641
|
+
* caller expressed no opinion, because the thirteen MCPs that construct this
|
|
37642
|
+
* class are separate packages: an operator whose extension re-install has
|
|
37643
|
+
* just locked all of them out needs one lever that does not require patching
|
|
37644
|
+
* every one of them.
|
|
37645
|
+
*/
|
|
37646
|
+
extensionTrust() {
|
|
37647
|
+
return fileExtensionTrust({
|
|
37648
|
+
serverName: this.opts.serverName,
|
|
37649
|
+
dir: this.opts.identityDir,
|
|
37650
|
+
allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
|
|
37651
|
+
});
|
|
37652
|
+
}
|
|
37286
37653
|
noteActivityForKeepalive() {
|
|
37287
37654
|
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
37288
37655
|
if (intervalMs <= 0)
|
|
@@ -37346,6 +37713,7 @@ var FetchproxyServer = class {
|
|
|
37346
37713
|
this.pending.delete(id);
|
|
37347
37714
|
this.pendingReadCookies.delete(id);
|
|
37348
37715
|
this.pendingStorage.delete(id);
|
|
37716
|
+
this.pendingWriteCookies.delete(id);
|
|
37349
37717
|
this.pendingCapture.delete(id);
|
|
37350
37718
|
this.pendingRedirect.delete(id);
|
|
37351
37719
|
this.pendingDownload.delete(id);
|
|
@@ -37467,7 +37835,7 @@ var FetchproxyServer = class {
|
|
|
37467
37835
|
port: this.opts.port
|
|
37468
37836
|
});
|
|
37469
37837
|
}
|
|
37470
|
-
return
|
|
37838
|
+
return protocolErrorFrom(result.error);
|
|
37471
37839
|
}
|
|
37472
37840
|
/**
|
|
37473
37841
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -37503,10 +37871,20 @@ var FetchproxyServer = class {
|
|
|
37503
37871
|
}
|
|
37504
37872
|
const url2 = isAbsolute ? path : `https://${host}${path}`;
|
|
37505
37873
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
37874
|
+
let tabUrl = `https://${host}/`;
|
|
37875
|
+
if (opts.viaTab !== void 0) {
|
|
37876
|
+
try {
|
|
37877
|
+
new URL(opts.viaTab);
|
|
37878
|
+
} catch {
|
|
37879
|
+
throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
|
|
37880
|
+
}
|
|
37881
|
+
assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
|
|
37882
|
+
tabUrl = opts.viaTab;
|
|
37883
|
+
}
|
|
37506
37884
|
const init = {
|
|
37507
37885
|
url: url2,
|
|
37508
37886
|
method,
|
|
37509
|
-
tabUrl
|
|
37887
|
+
tabUrl,
|
|
37510
37888
|
headers: opts.headers,
|
|
37511
37889
|
body: opts.body
|
|
37512
37890
|
};
|
|
@@ -37718,9 +38096,14 @@ var FetchproxyServer = class {
|
|
|
37718
38096
|
let inner;
|
|
37719
38097
|
if (opts.keys !== void 0) {
|
|
37720
38098
|
this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
|
|
38099
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
37721
38100
|
const initV3 = {
|
|
38101
|
+
// Origin stays BARE. The path travels as its own validated field —
|
|
38102
|
+
// `assertHttpsOriginOnly` deliberately refuses a path here so one
|
|
38103
|
+
// cannot be used to re-point the read past the domain gate.
|
|
37722
38104
|
origin: `https://${host}`,
|
|
37723
|
-
keys: [...opts.keys]
|
|
38105
|
+
keys: [...opts.keys],
|
|
38106
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
37724
38107
|
};
|
|
37725
38108
|
inner = { type: "request", id, op: "read_cookies", init: initV3 };
|
|
37726
38109
|
} else {
|
|
@@ -37733,10 +38116,65 @@ var FetchproxyServer = class {
|
|
|
37733
38116
|
await this.sendInnerFrame(inner);
|
|
37734
38117
|
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
37735
38118
|
if (!result.ok) {
|
|
37736
|
-
throw
|
|
38119
|
+
throw protocolErrorFrom(result.error);
|
|
37737
38120
|
}
|
|
37738
38121
|
return result.cookies;
|
|
37739
38122
|
}
|
|
38123
|
+
/**
|
|
38124
|
+
* 1.12.0+: overwrite the value of cookies this MCP already declares.
|
|
38125
|
+
*
|
|
38126
|
+
* The bridge's only write verb, and it exists for one failure class. Sites
|
|
38127
|
+
* that ROTATE a credential cookie hand back a new value on every refresh; if
|
|
38128
|
+
* the MCP refreshes and keeps the result to itself, the copy in the browser's
|
|
38129
|
+
* cookie jar is dead, and the user gets signed out of a tab they never
|
|
38130
|
+
* touched — usually reported to them as "inactivity". Writing the rotated
|
|
38131
|
+
* value back is the only thing that repairs it.
|
|
38132
|
+
*
|
|
38133
|
+
* Requires `'write_cookies'` in capabilities, which the user approves at pair
|
|
38134
|
+
* time as its own line. Every name must ALSO be in declared `cookieKeys`: a
|
|
38135
|
+
* write can never reach a cookie the MCP was not already trusted to read, so
|
|
38136
|
+
* granting it cannot widen which cookies are in play — only what may be done
|
|
38137
|
+
* to the ones already listed.
|
|
38138
|
+
*
|
|
38139
|
+
* The extension refuses the whole request unless every named cookie already
|
|
38140
|
+
* exists; this refreshes a value in place and deliberately cannot author new
|
|
38141
|
+
* cookies. Returns the names actually written.
|
|
38142
|
+
*/
|
|
38143
|
+
async writeCookies(opts) {
|
|
38144
|
+
if (!this.opts.capabilities.includes("write_cookies")) {
|
|
38145
|
+
throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
38146
|
+
}
|
|
38147
|
+
const names = Object.keys(opts.cookies);
|
|
38148
|
+
if (names.length === 0) {
|
|
38149
|
+
throw new Error("FetchproxyServer.writeCookies(): no cookies given");
|
|
38150
|
+
}
|
|
38151
|
+
await this.ensureConnected();
|
|
38152
|
+
this.throwIfPendingPair();
|
|
38153
|
+
if (opts.subdomain !== void 0)
|
|
38154
|
+
assertSubdomainLabel(opts.subdomain);
|
|
38155
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
38156
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
38157
|
+
this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
|
|
38158
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
38159
|
+
const id = this.nextRequestId++;
|
|
38160
|
+
const inner = {
|
|
38161
|
+
type: "request",
|
|
38162
|
+
id,
|
|
38163
|
+
op: "write_cookies",
|
|
38164
|
+
init: {
|
|
38165
|
+
// Bare origin, same invariant as the read path: a path must never be
|
|
38166
|
+
// able to move the request past the domain gate.
|
|
38167
|
+
origin: `https://${host}`,
|
|
38168
|
+
cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
|
|
38169
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
38170
|
+
}
|
|
38171
|
+
};
|
|
38172
|
+
const pending = new Promise((resolve, reject) => {
|
|
38173
|
+
this.pendingWriteCookies.set(id, { resolve, reject });
|
|
38174
|
+
});
|
|
38175
|
+
await this.sendInnerFrame(inner);
|
|
38176
|
+
return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
|
|
38177
|
+
}
|
|
37740
38178
|
/**
|
|
37741
38179
|
* 0.3.0+: read declared localStorage keys from the user's signed-in
|
|
37742
38180
|
* tab. Requires `'read_local_storage'` in capabilities AND each key
|
|
@@ -38283,7 +38721,7 @@ var FetchproxyServer = class {
|
|
|
38283
38721
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
38284
38722
|
}
|
|
38285
38723
|
} else {
|
|
38286
|
-
storageCb.reject(
|
|
38724
|
+
storageCb.reject(protocolErrorFrom(inner.error));
|
|
38287
38725
|
}
|
|
38288
38726
|
return;
|
|
38289
38727
|
}
|
|
@@ -38297,7 +38735,7 @@ var FetchproxyServer = class {
|
|
|
38297
38735
|
captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
|
|
38298
38736
|
}
|
|
38299
38737
|
} else {
|
|
38300
|
-
captureCb.reject(
|
|
38738
|
+
captureCb.reject(protocolErrorFrom(inner.error));
|
|
38301
38739
|
}
|
|
38302
38740
|
return;
|
|
38303
38741
|
}
|
|
@@ -38311,7 +38749,7 @@ var FetchproxyServer = class {
|
|
|
38311
38749
|
redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
|
|
38312
38750
|
}
|
|
38313
38751
|
} else {
|
|
38314
|
-
redirectCb.reject(
|
|
38752
|
+
redirectCb.reject(protocolErrorFrom(inner.error));
|
|
38315
38753
|
}
|
|
38316
38754
|
return;
|
|
38317
38755
|
}
|
|
@@ -38325,7 +38763,7 @@ var FetchproxyServer = class {
|
|
|
38325
38763
|
idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
|
|
38326
38764
|
}
|
|
38327
38765
|
} else {
|
|
38328
|
-
idbCb.reject(
|
|
38766
|
+
idbCb.reject(protocolErrorFrom(inner.error));
|
|
38329
38767
|
}
|
|
38330
38768
|
return;
|
|
38331
38769
|
}
|
|
@@ -38339,7 +38777,7 @@ var FetchproxyServer = class {
|
|
|
38339
38777
|
downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
|
|
38340
38778
|
}
|
|
38341
38779
|
} else {
|
|
38342
|
-
downloadCb.reject(
|
|
38780
|
+
downloadCb.reject(protocolErrorFrom(inner.error));
|
|
38343
38781
|
}
|
|
38344
38782
|
return;
|
|
38345
38783
|
}
|
|
@@ -38353,7 +38791,17 @@ var FetchproxyServer = class {
|
|
|
38353
38791
|
graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
|
|
38354
38792
|
}
|
|
38355
38793
|
} else {
|
|
38356
|
-
graphqlCb.reject(
|
|
38794
|
+
graphqlCb.reject(protocolErrorFrom(inner.error));
|
|
38795
|
+
}
|
|
38796
|
+
return;
|
|
38797
|
+
}
|
|
38798
|
+
const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
|
|
38799
|
+
if (writeCookiesCb) {
|
|
38800
|
+
this.pendingWriteCookies.delete(inner.id);
|
|
38801
|
+
if (inner.ok && inner.op === "write_cookies") {
|
|
38802
|
+
writeCookiesCb.resolve([...inner.written]);
|
|
38803
|
+
} else {
|
|
38804
|
+
writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
|
|
38357
38805
|
}
|
|
38358
38806
|
return;
|
|
38359
38807
|
}
|
|
@@ -38396,6 +38844,9 @@ var FetchproxyServer = class {
|
|
|
38396
38844
|
for (const { reject } of this.pendingStorage.values())
|
|
38397
38845
|
reject(err);
|
|
38398
38846
|
this.pendingStorage.clear();
|
|
38847
|
+
for (const { reject } of this.pendingWriteCookies.values())
|
|
38848
|
+
reject(err);
|
|
38849
|
+
this.pendingWriteCookies.clear();
|
|
38399
38850
|
for (const { reject } of this.pendingCapture.values())
|
|
38400
38851
|
reject(err);
|
|
38401
38852
|
this.pendingCapture.clear();
|
|
@@ -38546,7 +38997,9 @@ async function runOneLift(opts) {
|
|
|
38546
38997
|
if (opts.declare.cookies.length > 0) {
|
|
38547
38998
|
const joined = await server.readCookies({
|
|
38548
38999
|
keys: opts.declare.cookies,
|
|
38549
|
-
...storageDomainOpts
|
|
39000
|
+
...storageDomainOpts,
|
|
39001
|
+
// Cookie-only: the other buckets are origin-scoped and ignore path.
|
|
39002
|
+
...opts.storagePath !== void 0 ? { path: opts.storagePath } : {}
|
|
38550
39003
|
});
|
|
38551
39004
|
for (const piece of joined.split("; ")) {
|
|
38552
39005
|
if (!piece)
|
|
@@ -38720,7 +39173,7 @@ function loadAccount(env = process.env) {
|
|
|
38720
39173
|
// package.json
|
|
38721
39174
|
var package_default = {
|
|
38722
39175
|
name: "canvas-parent-mcp",
|
|
38723
|
-
version: "1.2.
|
|
39176
|
+
version: "1.2.5",
|
|
38724
39177
|
mcpName: "io.github.chrischall/canvas-parent-mcp",
|
|
38725
39178
|
description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
|
|
38726
39179
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38765,8 +39218,8 @@ var package_default = {
|
|
|
38765
39218
|
},
|
|
38766
39219
|
dependencies: {
|
|
38767
39220
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
38768
|
-
"@fetchproxy/bootstrap": "^
|
|
38769
|
-
"@fetchproxy/server": "^
|
|
39221
|
+
"@fetchproxy/bootstrap": "^2.0.0",
|
|
39222
|
+
"@fetchproxy/server": "^2.0.0",
|
|
38770
39223
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38771
39224
|
dotenv: "^17.4.2",
|
|
38772
39225
|
zod: "^4.4.3"
|
|
@@ -38873,7 +39326,7 @@ async function liftBrowserCookie(declaredDomain, baseHost) {
|
|
|
38873
39326
|
}
|
|
38874
39327
|
|
|
38875
39328
|
// src/client.ts
|
|
38876
|
-
import { writeFile as
|
|
39329
|
+
import { writeFile as writeFile3, stat } from "fs/promises";
|
|
38877
39330
|
import { dirname } from "path";
|
|
38878
39331
|
|
|
38879
39332
|
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
@@ -39186,7 +39639,7 @@ var CanvasClient = class {
|
|
|
39186
39639
|
if (res.status === 404) throw new Error(`Canvas download 404 for ${path}`);
|
|
39187
39640
|
if (!res.ok) throw new Error(`Canvas download ${res.status} for ${path}`);
|
|
39188
39641
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
39189
|
-
await
|
|
39642
|
+
await writeFile3(destinationPath, buf);
|
|
39190
39643
|
return {
|
|
39191
39644
|
path: destinationPath,
|
|
39192
39645
|
bytes: buf.byteLength,
|
|
@@ -39710,7 +40163,7 @@ function registerFileTools(server, client) {
|
|
|
39710
40163
|
|
|
39711
40164
|
// src/index.ts
|
|
39712
40165
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
39713
|
-
await loadDotenvSafely({ path:
|
|
40166
|
+
await loadDotenvSafely({ path: join3(__dirname, "..", ".env"), override: false });
|
|
39714
40167
|
var resolved = null;
|
|
39715
40168
|
var configError = null;
|
|
39716
40169
|
try {
|
|
@@ -39720,7 +40173,7 @@ try {
|
|
|
39720
40173
|
}
|
|
39721
40174
|
await runMcp({
|
|
39722
40175
|
name: "canvas",
|
|
39723
|
-
version: "1.2.
|
|
40176
|
+
version: "1.2.5",
|
|
39724
40177
|
// x-release-please-version
|
|
39725
40178
|
tools: [
|
|
39726
40179
|
(server) => {
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-parent-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"mcpName": "io.github.chrischall/canvas-parent-mcp",
|
|
5
5
|
"description": "Canvas LMS (Instructure) MCP server — student/observer read access with optional file download",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
48
|
-
"@fetchproxy/bootstrap": "^
|
|
49
|
-
"@fetchproxy/server": "^
|
|
48
|
+
"@fetchproxy/bootstrap": "^2.0.0",
|
|
49
|
+
"@fetchproxy/server": "^2.0.0",
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
51
|
"dotenv": "^17.4.2",
|
|
52
52
|
"zod": "^4.4.3"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/canvas-parent-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.2.
|
|
9
|
+
"version": "1.2.5",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "canvas-parent-mcp",
|
|
14
|
-
"version": "1.2.
|
|
14
|
+
"version": "1.2.5",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|