canvas-parent-mcp 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +953 -182
- package/dist/client.js +3 -0
- package/dist/index.js +1 -1
- package/dist/session-cache.js +91 -0
- package/mint.yaml +84 -0
- package/package.json +10 -7
- package/server.json +14 -2
- package/skills/canvas-parent/SKILL.md +1 -1
package/dist/bundle.js
CHANGED
|
@@ -2991,7 +2991,7 @@ var require_compile = __commonJS({
|
|
|
2991
2991
|
const schOrFunc = root.refs[ref];
|
|
2992
2992
|
if (schOrFunc)
|
|
2993
2993
|
return schOrFunc;
|
|
2994
|
-
let _sch =
|
|
2994
|
+
let _sch = resolve2.call(this, root, ref);
|
|
2995
2995
|
if (_sch === void 0) {
|
|
2996
2996
|
const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
2997
2997
|
const { schemaId } = this.opts;
|
|
@@ -3018,7 +3018,7 @@ var require_compile = __commonJS({
|
|
|
3018
3018
|
function sameSchemaEnv(s1, s2) {
|
|
3019
3019
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3020
3020
|
}
|
|
3021
|
-
function
|
|
3021
|
+
function resolve2(root, ref) {
|
|
3022
3022
|
let sch;
|
|
3023
3023
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3024
3024
|
ref = sch;
|
|
@@ -3649,9 +3649,14 @@ var require_fast_uri = __commonJS({
|
|
|
3649
3649
|
}
|
|
3650
3650
|
return uri;
|
|
3651
3651
|
}
|
|
3652
|
-
function
|
|
3652
|
+
function resolve2(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];
|
|
@@ -3913,7 +3933,7 @@ var require_fast_uri = __commonJS({
|
|
|
3913
3933
|
var fastUri = {
|
|
3914
3934
|
SCHEMES,
|
|
3915
3935
|
normalize,
|
|
3916
|
-
resolve,
|
|
3936
|
+
resolve: resolve2,
|
|
3917
3937
|
resolveComponent,
|
|
3918
3938
|
equal,
|
|
3919
3939
|
serialize,
|
|
@@ -9141,7 +9161,7 @@ var require_websocket = __commonJS({
|
|
|
9141
9161
|
var http = __require("http");
|
|
9142
9162
|
var net = __require("net");
|
|
9143
9163
|
var tls = __require("tls");
|
|
9144
|
-
var { randomBytes, createHash } = __require("crypto");
|
|
9164
|
+
var { randomBytes: randomBytes2, createHash } = __require("crypto");
|
|
9145
9165
|
var { Duplex, Readable } = __require("stream");
|
|
9146
9166
|
var { URL: URL2 } = __require("url");
|
|
9147
9167
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -9679,7 +9699,7 @@ var require_websocket = __commonJS({
|
|
|
9679
9699
|
}
|
|
9680
9700
|
}
|
|
9681
9701
|
const defaultPort = isSecure ? 443 : 80;
|
|
9682
|
-
const key =
|
|
9702
|
+
const key = randomBytes2(16).toString("base64");
|
|
9683
9703
|
const request = isSecure ? https.request : http.request;
|
|
9684
9704
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
9685
9705
|
let perMessageDeflate;
|
|
@@ -10573,7 +10593,7 @@ var require_websocket_server = __commonJS({
|
|
|
10573
10593
|
});
|
|
10574
10594
|
|
|
10575
10595
|
// src/index.ts
|
|
10576
|
-
import { dirname as
|
|
10596
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
10577
10597
|
import { fileURLToPath } from "url";
|
|
10578
10598
|
|
|
10579
10599
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -32534,7 +32554,7 @@ var Protocol = class {
|
|
|
32534
32554
|
return;
|
|
32535
32555
|
}
|
|
32536
32556
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
32537
|
-
await new Promise((
|
|
32557
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
32538
32558
|
options?.signal?.throwIfAborted();
|
|
32539
32559
|
}
|
|
32540
32560
|
} catch (error51) {
|
|
@@ -32551,7 +32571,7 @@ var Protocol = class {
|
|
|
32551
32571
|
*/
|
|
32552
32572
|
request(request, resultSchema, options) {
|
|
32553
32573
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
32554
|
-
return new Promise((
|
|
32574
|
+
return new Promise((resolve2, reject) => {
|
|
32555
32575
|
const earlyReject = (error51) => {
|
|
32556
32576
|
reject(error51);
|
|
32557
32577
|
};
|
|
@@ -32629,7 +32649,7 @@ var Protocol = class {
|
|
|
32629
32649
|
if (!parseResult.success) {
|
|
32630
32650
|
reject(parseResult.error);
|
|
32631
32651
|
} else {
|
|
32632
|
-
|
|
32652
|
+
resolve2(parseResult.data);
|
|
32633
32653
|
}
|
|
32634
32654
|
} catch (error51) {
|
|
32635
32655
|
reject(error51);
|
|
@@ -32890,12 +32910,12 @@ var Protocol = class {
|
|
|
32890
32910
|
}
|
|
32891
32911
|
} catch {
|
|
32892
32912
|
}
|
|
32893
|
-
return new Promise((
|
|
32913
|
+
return new Promise((resolve2, reject) => {
|
|
32894
32914
|
if (signal.aborted) {
|
|
32895
32915
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
32896
32916
|
return;
|
|
32897
32917
|
}
|
|
32898
|
-
const timeoutId = setTimeout(
|
|
32918
|
+
const timeoutId = setTimeout(resolve2, interval);
|
|
32899
32919
|
signal.addEventListener("abort", () => {
|
|
32900
32920
|
clearTimeout(timeoutId);
|
|
32901
32921
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -33986,7 +34006,7 @@ var McpServer = class {
|
|
|
33986
34006
|
let task = createTaskResult.task;
|
|
33987
34007
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
33988
34008
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
33989
|
-
await new Promise((
|
|
34009
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
33990
34010
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
33991
34011
|
if (!updatedTask) {
|
|
33992
34012
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -34650,64 +34670,17 @@ var StdioServerTransport = class {
|
|
|
34650
34670
|
this.onclose?.();
|
|
34651
34671
|
}
|
|
34652
34672
|
send(message) {
|
|
34653
|
-
return new Promise((
|
|
34673
|
+
return new Promise((resolve2) => {
|
|
34654
34674
|
const json2 = serializeMessage(message);
|
|
34655
34675
|
if (this._stdout.write(json2)) {
|
|
34656
|
-
|
|
34676
|
+
resolve2();
|
|
34657
34677
|
} else {
|
|
34658
|
-
this._stdout.once("drain",
|
|
34678
|
+
this._stdout.once("drain", resolve2);
|
|
34659
34679
|
}
|
|
34660
34680
|
});
|
|
34661
34681
|
}
|
|
34662
34682
|
};
|
|
34663
34683
|
|
|
34664
|
-
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
34665
|
-
async function createMcpServer(opts) {
|
|
34666
|
-
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
34667
|
-
if (opts.banner !== void 0) {
|
|
34668
|
-
console.error(opts.banner);
|
|
34669
|
-
}
|
|
34670
|
-
const deps = opts.deps;
|
|
34671
|
-
for (const register of opts.tools) {
|
|
34672
|
-
await register(server, deps);
|
|
34673
|
-
}
|
|
34674
|
-
return server;
|
|
34675
|
-
}
|
|
34676
|
-
function withGracefulShutdown(server, opts = {}) {
|
|
34677
|
-
const shouldExit = opts.exit ?? true;
|
|
34678
|
-
let shuttingDown = false;
|
|
34679
|
-
const handler = (signal) => {
|
|
34680
|
-
if (shuttingDown)
|
|
34681
|
-
return;
|
|
34682
|
-
shuttingDown = true;
|
|
34683
|
-
void (async () => {
|
|
34684
|
-
try {
|
|
34685
|
-
if (opts.onSignal)
|
|
34686
|
-
await opts.onSignal(signal);
|
|
34687
|
-
await server.close();
|
|
34688
|
-
} catch (err) {
|
|
34689
|
-
console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
34690
|
-
} finally {
|
|
34691
|
-
if (shouldExit)
|
|
34692
|
-
process.exit(0);
|
|
34693
|
-
}
|
|
34694
|
-
})();
|
|
34695
|
-
};
|
|
34696
|
-
process.on("SIGINT", () => handler("SIGINT"));
|
|
34697
|
-
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
34698
|
-
}
|
|
34699
|
-
async function runMcp(opts) {
|
|
34700
|
-
const server = await createMcpServer(opts);
|
|
34701
|
-
const shutdown = opts.shutdown ?? true;
|
|
34702
|
-
if (shutdown !== false) {
|
|
34703
|
-
withGracefulShutdown(server, shutdown === true ? {} : shutdown);
|
|
34704
|
-
}
|
|
34705
|
-
const spec = opts.transport ?? "stdio";
|
|
34706
|
-
const transport = spec === "stdio" ? new StdioServerTransport() : spec;
|
|
34707
|
-
await server.connect(transport);
|
|
34708
|
-
return server;
|
|
34709
|
-
}
|
|
34710
|
-
|
|
34711
34684
|
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
34712
34685
|
var DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
34713
34686
|
var McpToolError = class extends Error {
|
|
@@ -34765,6 +34738,81 @@ function textResult(data) {
|
|
|
34765
34738
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
34766
34739
|
};
|
|
34767
34740
|
}
|
|
34741
|
+
function errorResult(message) {
|
|
34742
|
+
return {
|
|
34743
|
+
content: [{ type: "text", text: redactSecrets(message) }],
|
|
34744
|
+
isError: true
|
|
34745
|
+
};
|
|
34746
|
+
}
|
|
34747
|
+
|
|
34748
|
+
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
34749
|
+
function hintResultOrRethrow(err) {
|
|
34750
|
+
if (err instanceof McpToolError && err.hint) {
|
|
34751
|
+
return errorResult(`${err.message}
|
|
34752
|
+
|
|
34753
|
+
Hint: ${err.hint}`);
|
|
34754
|
+
}
|
|
34755
|
+
throw err;
|
|
34756
|
+
}
|
|
34757
|
+
function surfaceToolHints(server) {
|
|
34758
|
+
const register = server.registerTool.bind(server);
|
|
34759
|
+
server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
|
|
34760
|
+
let result;
|
|
34761
|
+
try {
|
|
34762
|
+
result = cb(...args);
|
|
34763
|
+
} catch (err) {
|
|
34764
|
+
return hintResultOrRethrow(err);
|
|
34765
|
+
}
|
|
34766
|
+
return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
|
|
34767
|
+
});
|
|
34768
|
+
}
|
|
34769
|
+
async function createMcpServer(opts) {
|
|
34770
|
+
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
34771
|
+
if (opts.surfaceHints !== false)
|
|
34772
|
+
surfaceToolHints(server);
|
|
34773
|
+
if (opts.banner !== void 0) {
|
|
34774
|
+
console.error(opts.banner);
|
|
34775
|
+
}
|
|
34776
|
+
const deps = opts.deps;
|
|
34777
|
+
for (const register of opts.tools) {
|
|
34778
|
+
await register(server, deps);
|
|
34779
|
+
}
|
|
34780
|
+
return server;
|
|
34781
|
+
}
|
|
34782
|
+
function withGracefulShutdown(server, opts = {}) {
|
|
34783
|
+
const shouldExit = opts.exit ?? true;
|
|
34784
|
+
let shuttingDown = false;
|
|
34785
|
+
const handler = (signal) => {
|
|
34786
|
+
if (shuttingDown)
|
|
34787
|
+
return;
|
|
34788
|
+
shuttingDown = true;
|
|
34789
|
+
void (async () => {
|
|
34790
|
+
try {
|
|
34791
|
+
if (opts.onSignal)
|
|
34792
|
+
await opts.onSignal(signal);
|
|
34793
|
+
await server.close();
|
|
34794
|
+
} catch (err) {
|
|
34795
|
+
console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
34796
|
+
} finally {
|
|
34797
|
+
if (shouldExit)
|
|
34798
|
+
process.exit(0);
|
|
34799
|
+
}
|
|
34800
|
+
})();
|
|
34801
|
+
};
|
|
34802
|
+
process.on("SIGINT", () => handler("SIGINT"));
|
|
34803
|
+
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
34804
|
+
}
|
|
34805
|
+
async function runMcp(opts) {
|
|
34806
|
+
const server = await createMcpServer(opts);
|
|
34807
|
+
const shutdown = opts.shutdown ?? true;
|
|
34808
|
+
if (shutdown !== false) {
|
|
34809
|
+
withGracefulShutdown(server, shutdown === true ? {} : shutdown);
|
|
34810
|
+
}
|
|
34811
|
+
const spec = opts.transport ?? "stdio";
|
|
34812
|
+
const transport = spec === "stdio" ? new StdioServerTransport() : spec;
|
|
34813
|
+
await server.connect(transport);
|
|
34814
|
+
return server;
|
|
34815
|
+
}
|
|
34768
34816
|
|
|
34769
34817
|
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
34770
34818
|
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
@@ -34779,6 +34827,20 @@ function readEnvVar(key, opts = {}) {
|
|
|
34779
34827
|
}
|
|
34780
34828
|
return opts.default;
|
|
34781
34829
|
}
|
|
34830
|
+
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
34831
|
+
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
34832
|
+
function parseBoolEnv(key, opts = {}) {
|
|
34833
|
+
const fallback = opts.default ?? false;
|
|
34834
|
+
const raw = readEnvVar(key, { env: opts.env });
|
|
34835
|
+
if (raw === void 0)
|
|
34836
|
+
return fallback;
|
|
34837
|
+
const token = raw.toLowerCase();
|
|
34838
|
+
if (TRUE_TOKENS.has(token))
|
|
34839
|
+
return true;
|
|
34840
|
+
if (FALSE_TOKENS.has(token))
|
|
34841
|
+
return false;
|
|
34842
|
+
return fallback;
|
|
34843
|
+
}
|
|
34782
34844
|
async function loadDotenvSafely(opts = {}) {
|
|
34783
34845
|
try {
|
|
34784
34846
|
const mod = await import(
|
|
@@ -34998,8 +35060,15 @@ function createOAuth2Refresher(opts) {
|
|
|
34998
35060
|
}
|
|
34999
35061
|
|
|
35000
35062
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
35001
|
-
var PROTOCOL_VERSION =
|
|
35063
|
+
var PROTOCOL_VERSION = 3;
|
|
35002
35064
|
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
35065
|
+
function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
|
|
35066
|
+
const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
|
|
35067
|
+
out.set(mcpHelloNonce, 0);
|
|
35068
|
+
out.set(extHelloNonce, mcpHelloNonce.length);
|
|
35069
|
+
out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
|
|
35070
|
+
return out;
|
|
35071
|
+
}
|
|
35003
35072
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
35004
35073
|
"fetch",
|
|
35005
35074
|
"read_cookies",
|
|
@@ -35010,7 +35079,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
35010
35079
|
"read_indexed_db",
|
|
35011
35080
|
"read_dom",
|
|
35012
35081
|
"download",
|
|
35013
|
-
"graphql"
|
|
35082
|
+
"graphql",
|
|
35083
|
+
"write_cookies"
|
|
35014
35084
|
]);
|
|
35015
35085
|
|
|
35016
35086
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35121,6 +35191,15 @@ function assertHttpUrl(x, label) {
|
|
|
35121
35191
|
throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
|
|
35122
35192
|
}
|
|
35123
35193
|
}
|
|
35194
|
+
function assertCookiePath(x, label) {
|
|
35195
|
+
assertString(x, label);
|
|
35196
|
+
if (!x.startsWith("/") || x.startsWith("//")) {
|
|
35197
|
+
throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
|
|
35198
|
+
}
|
|
35199
|
+
if (x.includes("?") || x.includes("#") || x.includes("\\")) {
|
|
35200
|
+
throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
|
|
35201
|
+
}
|
|
35202
|
+
}
|
|
35124
35203
|
function assertHttpsOriginOnly(x, label) {
|
|
35125
35204
|
assertString(x, label);
|
|
35126
35205
|
let u;
|
|
@@ -35565,13 +35644,43 @@ function validateInnerRequest(raw) {
|
|
|
35565
35644
|
}
|
|
35566
35645
|
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35567
35646
|
assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
|
|
35647
|
+
if (raw.init.path !== void 0)
|
|
35648
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35568
35649
|
for (const k of Object.keys(raw.init)) {
|
|
35569
|
-
if (k !== "origin" && k !== "keys") {
|
|
35650
|
+
if (k !== "origin" && k !== "keys" && k !== "path") {
|
|
35570
35651
|
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
|
|
35571
35652
|
}
|
|
35572
35653
|
}
|
|
35573
35654
|
return raw;
|
|
35574
35655
|
}
|
|
35656
|
+
if (raw.op === "write_cookies") {
|
|
35657
|
+
assertObject(raw.init, "inner.init");
|
|
35658
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35659
|
+
if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
|
|
35660
|
+
throw new ProtocolError("inner.init.cookies: must be a non-empty array");
|
|
35661
|
+
}
|
|
35662
|
+
for (const [i, entry] of raw.init.cookies.entries()) {
|
|
35663
|
+
assertObject(entry, `inner.init.cookies[${i}]`);
|
|
35664
|
+
assertString(entry.name, `inner.init.cookies[${i}].name`);
|
|
35665
|
+
if (!SCOPE_KEY_RE.test(entry.name)) {
|
|
35666
|
+
throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
|
|
35667
|
+
}
|
|
35668
|
+
assertString(entry.value, `inner.init.cookies[${i}].value`);
|
|
35669
|
+
for (const k of Object.keys(entry)) {
|
|
35670
|
+
if (k !== "name" && k !== "value") {
|
|
35671
|
+
throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35672
|
+
}
|
|
35673
|
+
}
|
|
35674
|
+
}
|
|
35675
|
+
if (raw.init.path !== void 0)
|
|
35676
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35677
|
+
for (const k of Object.keys(raw.init)) {
|
|
35678
|
+
if (k !== "origin" && k !== "cookies" && k !== "path") {
|
|
35679
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
|
|
35680
|
+
}
|
|
35681
|
+
}
|
|
35682
|
+
return raw;
|
|
35683
|
+
}
|
|
35575
35684
|
if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
|
|
35576
35685
|
assertObject(raw.init, "inner.init");
|
|
35577
35686
|
if (raw.init.origin === void 0) {
|
|
@@ -35758,7 +35867,7 @@ function validateInnerRequest(raw) {
|
|
|
35758
35867
|
}
|
|
35759
35868
|
return raw;
|
|
35760
35869
|
}
|
|
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)}`);
|
|
35870
|
+
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
35871
|
}
|
|
35763
35872
|
function assertNonEmptyKeyArray(value, label) {
|
|
35764
35873
|
if (!Array.isArray(value)) {
|
|
@@ -35819,6 +35928,18 @@ function validateInnerResponse(raw) {
|
|
|
35819
35928
|
}
|
|
35820
35929
|
return raw;
|
|
35821
35930
|
}
|
|
35931
|
+
if (op === "write_cookies") {
|
|
35932
|
+
if (raw.written === void 0) {
|
|
35933
|
+
throw new ProtocolError("inner.written: missing on write_cookies response");
|
|
35934
|
+
}
|
|
35935
|
+
if (!Array.isArray(raw.written)) {
|
|
35936
|
+
throw new ProtocolError("inner.written: must be an array");
|
|
35937
|
+
}
|
|
35938
|
+
for (const [i, name] of raw.written.entries()) {
|
|
35939
|
+
assertString(name, `inner.written[${i}]`);
|
|
35940
|
+
}
|
|
35941
|
+
return raw;
|
|
35942
|
+
}
|
|
35822
35943
|
if (op === "read_local_storage" || op === "read_session_storage") {
|
|
35823
35944
|
if (raw.values === void 0) {
|
|
35824
35945
|
throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
|
|
@@ -36112,7 +36233,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
|
|
|
36112
36233
|
async function electRole(opts) {
|
|
36113
36234
|
const server = createServer();
|
|
36114
36235
|
const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
|
|
36115
|
-
return new Promise((
|
|
36236
|
+
return new Promise((resolve2, reject) => {
|
|
36116
36237
|
let timer;
|
|
36117
36238
|
const clearBindTimer = () => {
|
|
36118
36239
|
if (timer) {
|
|
@@ -36128,7 +36249,7 @@ async function electRole(opts) {
|
|
|
36128
36249
|
server.close();
|
|
36129
36250
|
} catch {
|
|
36130
36251
|
}
|
|
36131
|
-
|
|
36252
|
+
resolve2({ role: "peer" });
|
|
36132
36253
|
} else {
|
|
36133
36254
|
reject(e);
|
|
36134
36255
|
}
|
|
@@ -36136,7 +36257,7 @@ async function electRole(opts) {
|
|
|
36136
36257
|
const onListening = () => {
|
|
36137
36258
|
clearBindTimer();
|
|
36138
36259
|
server.removeListener("error", onError);
|
|
36139
|
-
|
|
36260
|
+
resolve2({ role: "host", server });
|
|
36140
36261
|
};
|
|
36141
36262
|
server.once("error", onError);
|
|
36142
36263
|
server.once("listening", onListening);
|
|
@@ -36296,6 +36417,148 @@ async function awaitSessionReady(ready, opts) {
|
|
|
36296
36417
|
}
|
|
36297
36418
|
}
|
|
36298
36419
|
|
|
36420
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36421
|
+
import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
36422
|
+
import { join as join2 } from "node:path";
|
|
36423
|
+
|
|
36424
|
+
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36425
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36426
|
+
import { join } from "node:path";
|
|
36427
|
+
import { homedir } from "node:os";
|
|
36428
|
+
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36429
|
+
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36430
|
+
function defaultIdentityDir() {
|
|
36431
|
+
return join(homedir(), ".fetchproxy", "identity");
|
|
36432
|
+
}
|
|
36433
|
+
function safeIdentityFileBase(serverName) {
|
|
36434
|
+
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36435
|
+
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36436
|
+
}
|
|
36437
|
+
return serverName.replace(/\//g, "_");
|
|
36438
|
+
}
|
|
36439
|
+
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36440
|
+
const safeFile = safeIdentityFileBase(serverName);
|
|
36441
|
+
const path = join(dir, `${safeFile}.json`);
|
|
36442
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36443
|
+
try {
|
|
36444
|
+
const raw = await readFile(path, "utf8");
|
|
36445
|
+
const j2 = JSON.parse(raw);
|
|
36446
|
+
return {
|
|
36447
|
+
x25519Priv: fromB64(j2.x25519Priv),
|
|
36448
|
+
x25519Pub: fromB64(j2.x25519Pub),
|
|
36449
|
+
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36450
|
+
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36451
|
+
createdAt: j2.createdAt
|
|
36452
|
+
};
|
|
36453
|
+
} catch (e) {
|
|
36454
|
+
if (e.code !== "ENOENT")
|
|
36455
|
+
throw e;
|
|
36456
|
+
}
|
|
36457
|
+
const x = await generateX25519();
|
|
36458
|
+
const ed = await generateEd25519();
|
|
36459
|
+
const id = {
|
|
36460
|
+
x25519Priv: x.privateKey,
|
|
36461
|
+
x25519Pub: x.publicKey,
|
|
36462
|
+
ed25519Priv: ed.privateKey,
|
|
36463
|
+
ed25519Pub: ed.publicKey,
|
|
36464
|
+
createdAt: Date.now()
|
|
36465
|
+
};
|
|
36466
|
+
const j = {
|
|
36467
|
+
x25519Priv: toB64(id.x25519Priv),
|
|
36468
|
+
x25519Pub: toB64(id.x25519Pub),
|
|
36469
|
+
ed25519Priv: toB64(id.ed25519Priv),
|
|
36470
|
+
ed25519Pub: toB64(id.ed25519Pub),
|
|
36471
|
+
createdAt: id.createdAt
|
|
36472
|
+
};
|
|
36473
|
+
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36474
|
+
await chmod(path, 384);
|
|
36475
|
+
return id;
|
|
36476
|
+
}
|
|
36477
|
+
|
|
36478
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36479
|
+
function fileExtensionTrust(args) {
|
|
36480
|
+
return {
|
|
36481
|
+
allowNew: args.allowNew,
|
|
36482
|
+
location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36483
|
+
read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36484
|
+
write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
|
|
36485
|
+
};
|
|
36486
|
+
}
|
|
36487
|
+
var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
|
|
36488
|
+
function allowNewExtensionIdentity(explicit, env = process.env) {
|
|
36489
|
+
if (explicit !== void 0)
|
|
36490
|
+
return explicit;
|
|
36491
|
+
return env[TRUST_NEW_EXTENSION_ENV] === "1";
|
|
36492
|
+
}
|
|
36493
|
+
function decideExtensionTrust(args) {
|
|
36494
|
+
const { pin, hello, allowNew, serverName } = args;
|
|
36495
|
+
if (!pin)
|
|
36496
|
+
return { decision: "first-use" };
|
|
36497
|
+
if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
|
|
36498
|
+
return { decision: "pinned" };
|
|
36499
|
+
}
|
|
36500
|
+
const trustPath = args.location ?? extensionTrustPathHint(serverName);
|
|
36501
|
+
if (allowNew) {
|
|
36502
|
+
return {
|
|
36503
|
+
decision: "replace",
|
|
36504
|
+
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.`
|
|
36505
|
+
};
|
|
36506
|
+
}
|
|
36507
|
+
return {
|
|
36508
|
+
decision: "refused",
|
|
36509
|
+
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.`
|
|
36510
|
+
};
|
|
36511
|
+
}
|
|
36512
|
+
function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
|
|
36513
|
+
return join2(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
|
|
36514
|
+
}
|
|
36515
|
+
function extensionTrustPathHint(serverName) {
|
|
36516
|
+
try {
|
|
36517
|
+
return extensionTrustPath(serverName);
|
|
36518
|
+
} catch {
|
|
36519
|
+
return join2(defaultIdentityDir(), "<server-name>.extension-trust.json");
|
|
36520
|
+
}
|
|
36521
|
+
}
|
|
36522
|
+
function isPin(x) {
|
|
36523
|
+
if (!x || typeof x !== "object")
|
|
36524
|
+
return false;
|
|
36525
|
+
const r = x;
|
|
36526
|
+
return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
|
|
36527
|
+
}
|
|
36528
|
+
async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
|
|
36529
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36530
|
+
let raw;
|
|
36531
|
+
try {
|
|
36532
|
+
raw = await readFile2(path, "utf8");
|
|
36533
|
+
} catch (e) {
|
|
36534
|
+
if (e.code === "ENOENT")
|
|
36535
|
+
return null;
|
|
36536
|
+
throw e;
|
|
36537
|
+
}
|
|
36538
|
+
let parsed;
|
|
36539
|
+
try {
|
|
36540
|
+
parsed = JSON.parse(raw);
|
|
36541
|
+
} catch {
|
|
36542
|
+
throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
|
|
36543
|
+
}
|
|
36544
|
+
if (!isPin(parsed)) {
|
|
36545
|
+
throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
|
|
36546
|
+
}
|
|
36547
|
+
return {
|
|
36548
|
+
identityX25519Pub: parsed.identityX25519Pub,
|
|
36549
|
+
identityEd25519Pub: parsed.identityEd25519Pub,
|
|
36550
|
+
pinnedAt: parsed.pinnedAt
|
|
36551
|
+
};
|
|
36552
|
+
}
|
|
36553
|
+
async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
|
|
36554
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36555
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
36556
|
+
const tmp = `${path}.tmp`;
|
|
36557
|
+
await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
|
|
36558
|
+
await chmod2(tmp, 384);
|
|
36559
|
+
await rename(tmp, path);
|
|
36560
|
+
}
|
|
36561
|
+
|
|
36299
36562
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
36300
36563
|
var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
36301
36564
|
var enc2 = new TextEncoder();
|
|
@@ -36340,8 +36603,8 @@ async function startHost(opts) {
|
|
|
36340
36603
|
let rejectOwnSession;
|
|
36341
36604
|
let ownSessionReady;
|
|
36342
36605
|
function resetSessionPromise() {
|
|
36343
|
-
ownSessionReady = new Promise((
|
|
36344
|
-
resolveOwnSession =
|
|
36606
|
+
ownSessionReady = new Promise((resolve2, reject) => {
|
|
36607
|
+
resolveOwnSession = resolve2;
|
|
36345
36608
|
rejectOwnSession = reject;
|
|
36346
36609
|
});
|
|
36347
36610
|
ownSessionReady.catch(() => {
|
|
@@ -36349,9 +36612,12 @@ async function startHost(opts) {
|
|
|
36349
36612
|
}
|
|
36350
36613
|
resetSessionPromise();
|
|
36351
36614
|
let extensionHello = null;
|
|
36615
|
+
let extensionClaim = null;
|
|
36352
36616
|
wss.on("connection", (ws) => {
|
|
36353
36617
|
let identified = null;
|
|
36354
36618
|
let peerMcpId = null;
|
|
36619
|
+
let closed = false;
|
|
36620
|
+
let pinOnReady = false;
|
|
36355
36621
|
ws.on("message", async (data) => {
|
|
36356
36622
|
try {
|
|
36357
36623
|
let frame;
|
|
@@ -36363,10 +36629,43 @@ async function startHost(opts) {
|
|
|
36363
36629
|
return;
|
|
36364
36630
|
}
|
|
36365
36631
|
if (frame.type === "hello" && frame.role === "extension") {
|
|
36366
|
-
if (extensionWs) {
|
|
36632
|
+
if (extensionWs || extensionClaim) {
|
|
36367
36633
|
ws.close(1008, "extension already connected");
|
|
36368
36634
|
return;
|
|
36369
36635
|
}
|
|
36636
|
+
extensionClaim = ws;
|
|
36637
|
+
let pin;
|
|
36638
|
+
try {
|
|
36639
|
+
pin = await opts.extensionTrust.read();
|
|
36640
|
+
} catch (e) {
|
|
36641
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36642
|
+
if (extensionClaim === ws)
|
|
36643
|
+
extensionClaim = null;
|
|
36644
|
+
ws.close(1008, "extension pin unreadable");
|
|
36645
|
+
return;
|
|
36646
|
+
}
|
|
36647
|
+
const outcome = decideExtensionTrust({
|
|
36648
|
+
pin,
|
|
36649
|
+
hello: frame,
|
|
36650
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36651
|
+
serverName: opts.ownServerName,
|
|
36652
|
+
location: opts.extensionTrust.location
|
|
36653
|
+
});
|
|
36654
|
+
if (outcome.decision === "refused") {
|
|
36655
|
+
console.warn(outcome.message);
|
|
36656
|
+
if (extensionClaim === ws)
|
|
36657
|
+
extensionClaim = null;
|
|
36658
|
+
ws.close(1008, "extension identity is not the pinned one");
|
|
36659
|
+
return;
|
|
36660
|
+
}
|
|
36661
|
+
if (outcome.decision === "replace")
|
|
36662
|
+
console.warn(outcome.message);
|
|
36663
|
+
if (closed || ws.readyState !== import_websocket.default.OPEN) {
|
|
36664
|
+
if (extensionClaim === ws)
|
|
36665
|
+
extensionClaim = null;
|
|
36666
|
+
return;
|
|
36667
|
+
}
|
|
36668
|
+
pinOnReady = outcome.decision !== "pinned";
|
|
36370
36669
|
identified = "extension";
|
|
36371
36670
|
extensionWs = ws;
|
|
36372
36671
|
extensionHello = frame;
|
|
@@ -36378,6 +36677,8 @@ async function startHost(opts) {
|
|
|
36378
36677
|
console.error("[fetchproxy] onPairCode threw:", e);
|
|
36379
36678
|
}
|
|
36380
36679
|
}
|
|
36680
|
+
for (const slot of peers.values())
|
|
36681
|
+
slot.ws.send(JSON.stringify(frame));
|
|
36381
36682
|
ws.send(JSON.stringify(ownHello));
|
|
36382
36683
|
for (const slot of peers.values()) {
|
|
36383
36684
|
ws.send(JSON.stringify(slot.helloFrame));
|
|
@@ -36413,6 +36714,8 @@ async function startHost(opts) {
|
|
|
36413
36714
|
peers.set(frame.mcpId, { ws, helloFrame: frame });
|
|
36414
36715
|
if (extensionWs)
|
|
36415
36716
|
extensionWs.send(JSON.stringify(frame));
|
|
36717
|
+
if (extensionHello)
|
|
36718
|
+
ws.send(JSON.stringify(extensionHello));
|
|
36416
36719
|
return;
|
|
36417
36720
|
}
|
|
36418
36721
|
if (frame.type === "ready") {
|
|
@@ -36424,7 +36727,7 @@ async function startHost(opts) {
|
|
|
36424
36727
|
}
|
|
36425
36728
|
const extEdPub = fromB64(extensionHello.identityEd25519Pub);
|
|
36426
36729
|
const extNonce = fromB64(extensionHello.sessionNonce);
|
|
36427
|
-
const msg =
|
|
36730
|
+
const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
|
|
36428
36731
|
const sig = fromB64(frame.sessionSig);
|
|
36429
36732
|
let sigOk = false;
|
|
36430
36733
|
try {
|
|
@@ -36437,6 +36740,18 @@ async function startHost(opts) {
|
|
|
36437
36740
|
ws.close(1008, "extension session signature invalid");
|
|
36438
36741
|
return;
|
|
36439
36742
|
}
|
|
36743
|
+
if (pinOnReady) {
|
|
36744
|
+
pinOnReady = false;
|
|
36745
|
+
try {
|
|
36746
|
+
await opts.extensionTrust.write({
|
|
36747
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36748
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36749
|
+
pinnedAt: Date.now()
|
|
36750
|
+
});
|
|
36751
|
+
} catch (e) {
|
|
36752
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36753
|
+
}
|
|
36754
|
+
}
|
|
36440
36755
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36441
36756
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
36442
36757
|
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36490,6 +36805,9 @@ async function startHost(opts) {
|
|
|
36490
36805
|
}
|
|
36491
36806
|
});
|
|
36492
36807
|
ws.on("close", () => {
|
|
36808
|
+
closed = true;
|
|
36809
|
+
if (extensionClaim === ws)
|
|
36810
|
+
extensionClaim = null;
|
|
36493
36811
|
if (identified === "extension" && extensionWs === ws) {
|
|
36494
36812
|
extensionWs = null;
|
|
36495
36813
|
extensionHello = null;
|
|
@@ -36507,7 +36825,7 @@ async function startHost(opts) {
|
|
|
36507
36825
|
});
|
|
36508
36826
|
});
|
|
36509
36827
|
return {
|
|
36510
|
-
close: () => new Promise((
|
|
36828
|
+
close: () => new Promise((resolve2) => {
|
|
36511
36829
|
for (const client of wss.clients) {
|
|
36512
36830
|
try {
|
|
36513
36831
|
client.terminate();
|
|
@@ -36515,7 +36833,7 @@ async function startHost(opts) {
|
|
|
36515
36833
|
}
|
|
36516
36834
|
}
|
|
36517
36835
|
wss.close(() => {
|
|
36518
|
-
opts.httpServer.close(() =>
|
|
36836
|
+
opts.httpServer.close(() => resolve2());
|
|
36519
36837
|
});
|
|
36520
36838
|
}),
|
|
36521
36839
|
sendOwnInner: async (inner) => {
|
|
@@ -36545,8 +36863,8 @@ async function startHost(opts) {
|
|
|
36545
36863
|
var enc3 = new TextEncoder();
|
|
36546
36864
|
async function startPeer(opts) {
|
|
36547
36865
|
const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
|
|
36548
|
-
await new Promise((
|
|
36549
|
-
ws.once("open", () =>
|
|
36866
|
+
await new Promise((resolve2, reject) => {
|
|
36867
|
+
ws.once("open", () => resolve2());
|
|
36550
36868
|
ws.once("error", reject);
|
|
36551
36869
|
});
|
|
36552
36870
|
const hello = await buildServerHello({
|
|
@@ -36576,15 +36894,88 @@ async function startPeer(opts) {
|
|
|
36576
36894
|
let pendingPairCode = null;
|
|
36577
36895
|
let resolveFirstReady;
|
|
36578
36896
|
let rejectFirstReady;
|
|
36579
|
-
const sessionPromise = new Promise((
|
|
36580
|
-
resolveFirstReady =
|
|
36897
|
+
const sessionPromise = new Promise((resolve2, reject) => {
|
|
36898
|
+
resolveFirstReady = resolve2;
|
|
36581
36899
|
rejectFirstReady = reject;
|
|
36582
36900
|
});
|
|
36901
|
+
let extensionHello = null;
|
|
36902
|
+
let warnedUnverifiable = false;
|
|
36903
|
+
let cachedPin = void 0;
|
|
36904
|
+
const authenticateExtension = async (sessionSig, extensionSessionPub) => {
|
|
36905
|
+
if (!extensionHello) {
|
|
36906
|
+
if (opts.requireExtensionIdentity) {
|
|
36907
|
+
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.`);
|
|
36908
|
+
return false;
|
|
36909
|
+
}
|
|
36910
|
+
if (!warnedUnverifiable) {
|
|
36911
|
+
warnedUnverifiable = true;
|
|
36912
|
+
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.`);
|
|
36913
|
+
}
|
|
36914
|
+
return true;
|
|
36915
|
+
}
|
|
36916
|
+
const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
|
|
36917
|
+
let sigOk = false;
|
|
36918
|
+
try {
|
|
36919
|
+
sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
|
|
36920
|
+
} catch {
|
|
36921
|
+
sigOk = false;
|
|
36922
|
+
}
|
|
36923
|
+
if (!sigOk) {
|
|
36924
|
+
console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
|
|
36925
|
+
return false;
|
|
36926
|
+
}
|
|
36927
|
+
if (cachedPin === void 0) {
|
|
36928
|
+
try {
|
|
36929
|
+
cachedPin = await opts.extensionTrust.read();
|
|
36930
|
+
} catch (e) {
|
|
36931
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36932
|
+
return false;
|
|
36933
|
+
}
|
|
36934
|
+
}
|
|
36935
|
+
const pin = cachedPin;
|
|
36936
|
+
const outcome = decideExtensionTrust({
|
|
36937
|
+
pin,
|
|
36938
|
+
hello: extensionHello,
|
|
36939
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36940
|
+
serverName: opts.serverName,
|
|
36941
|
+
location: opts.extensionTrust.location
|
|
36942
|
+
});
|
|
36943
|
+
if (outcome.decision === "refused") {
|
|
36944
|
+
console.warn(outcome.message);
|
|
36945
|
+
return false;
|
|
36946
|
+
}
|
|
36947
|
+
if (outcome.decision === "replace")
|
|
36948
|
+
console.warn(outcome.message);
|
|
36949
|
+
if (outcome.decision !== "pinned") {
|
|
36950
|
+
try {
|
|
36951
|
+
const written = {
|
|
36952
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36953
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36954
|
+
pinnedAt: Date.now()
|
|
36955
|
+
};
|
|
36956
|
+
await opts.extensionTrust.write(written);
|
|
36957
|
+
cachedPin = written;
|
|
36958
|
+
} catch (e) {
|
|
36959
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36960
|
+
}
|
|
36961
|
+
}
|
|
36962
|
+
return true;
|
|
36963
|
+
};
|
|
36583
36964
|
const onMessage = async (data) => {
|
|
36584
36965
|
try {
|
|
36585
36966
|
const raw = JSON.parse(data.toString());
|
|
36586
36967
|
const frame = validateFrame(raw);
|
|
36968
|
+
if (frame.type === "hello" && frame.role === "extension") {
|
|
36969
|
+
extensionHello = frame;
|
|
36970
|
+
return;
|
|
36971
|
+
}
|
|
36587
36972
|
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
36973
|
+
const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
|
|
36974
|
+
if (!authorised) {
|
|
36975
|
+
ws.close(1008, "extension identity refused");
|
|
36976
|
+
rejectFirstReady(new Error("peer: extension identity refused"));
|
|
36977
|
+
return;
|
|
36978
|
+
}
|
|
36588
36979
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36589
36980
|
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
36590
36981
|
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36665,57 +37056,6 @@ async function startPeer(opts) {
|
|
|
36665
37056
|
return handle;
|
|
36666
37057
|
}
|
|
36667
37058
|
|
|
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
37059
|
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
36720
37060
|
function classifyFetchError(error51) {
|
|
36721
37061
|
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
@@ -36756,6 +37096,17 @@ function classifyBridgeError(err) {
|
|
|
36756
37096
|
}
|
|
36757
37097
|
|
|
36758
37098
|
// node_modules/@fetchproxy/server/dist/ws-server.js
|
|
37099
|
+
function envWsPort() {
|
|
37100
|
+
const raw = process.env.FETCHPROXY_WS_PORT;
|
|
37101
|
+
if (raw === void 0 || raw.trim() === "")
|
|
37102
|
+
return void 0;
|
|
37103
|
+
if (!/^\d+$/.test(raw.trim()))
|
|
37104
|
+
return void 0;
|
|
37105
|
+
const port = Number(raw.trim());
|
|
37106
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
37107
|
+
return void 0;
|
|
37108
|
+
return port;
|
|
37109
|
+
}
|
|
36759
37110
|
var FetchproxyProtocolError = class extends Error {
|
|
36760
37111
|
constructor(message) {
|
|
36761
37112
|
super(message);
|
|
@@ -36797,6 +37148,39 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
|
36797
37148
|
this.hint = hint;
|
|
36798
37149
|
}
|
|
36799
37150
|
};
|
|
37151
|
+
var FetchproxyHintedError = class extends FetchproxyProtocolError {
|
|
37152
|
+
/** The extension's raw rejection, unmodified. */
|
|
37153
|
+
originalError;
|
|
37154
|
+
/** What the user should actually do, in prose. */
|
|
37155
|
+
hint;
|
|
37156
|
+
constructor(originalError, hint) {
|
|
37157
|
+
super(`${originalError} \u2014 ${hint}`);
|
|
37158
|
+
this.name = "FetchproxyHintedError";
|
|
37159
|
+
this.originalError = originalError;
|
|
37160
|
+
this.hint = hint;
|
|
37161
|
+
}
|
|
37162
|
+
};
|
|
37163
|
+
var FetchproxyScopeError = class extends FetchproxyHintedError {
|
|
37164
|
+
constructor(originalError) {
|
|
37165
|
+
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.");
|
|
37166
|
+
this.name = "FetchproxyScopeError";
|
|
37167
|
+
}
|
|
37168
|
+
};
|
|
37169
|
+
var FetchproxyNoTabError = class extends FetchproxyHintedError {
|
|
37170
|
+
constructor(originalError) {
|
|
37171
|
+
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.");
|
|
37172
|
+
this.name = "FetchproxyNoTabError";
|
|
37173
|
+
}
|
|
37174
|
+
};
|
|
37175
|
+
var SCOPE_REJECTION = /not in declared/;
|
|
37176
|
+
var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
|
|
37177
|
+
function protocolErrorFrom(error51) {
|
|
37178
|
+
if (SCOPE_REJECTION.test(error51))
|
|
37179
|
+
return new FetchproxyScopeError(error51);
|
|
37180
|
+
if (NO_TAB_REJECTION.test(error51))
|
|
37181
|
+
return new FetchproxyNoTabError(error51);
|
|
37182
|
+
return new FetchproxyProtocolError(error51);
|
|
37183
|
+
}
|
|
36800
37184
|
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
36801
37185
|
url;
|
|
36802
37186
|
timeoutMs;
|
|
@@ -36826,6 +37210,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
36826
37210
|
}
|
|
36827
37211
|
};
|
|
36828
37212
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
37213
|
+
function normalizeCookiePath(path) {
|
|
37214
|
+
if (path === void 0 || path === "")
|
|
37215
|
+
return void 0;
|
|
37216
|
+
const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
37217
|
+
try {
|
|
37218
|
+
assertCookiePath(trimmed, "path");
|
|
37219
|
+
} catch (e) {
|
|
37220
|
+
throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
|
|
37221
|
+
}
|
|
37222
|
+
return trimmed;
|
|
37223
|
+
}
|
|
36829
37224
|
function assertSubdomainLabel(label) {
|
|
36830
37225
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
36831
37226
|
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 +37285,9 @@ var FetchproxyServer = class {
|
|
|
36890
37285
|
// them off from `pending` (fetch) and `pendingReadCookies` (legacy
|
|
36891
37286
|
// string-shape) so the response routing in `onInner` stays linear.
|
|
36892
37287
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
37288
|
+
// 1.12.0+: write-cookies awaiters resolve the list of names actually
|
|
37289
|
+
// written, so a caller can confirm rather than assume.
|
|
37290
|
+
pendingWriteCookies = /* @__PURE__ */ new Map();
|
|
36893
37291
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
36894
37292
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
36895
37293
|
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
@@ -36956,7 +37354,7 @@ var FetchproxyServer = class {
|
|
|
36956
37354
|
}
|
|
36957
37355
|
}
|
|
36958
37356
|
this.opts = {
|
|
36959
|
-
port: opts.port ?? 37149,
|
|
37357
|
+
port: opts.port ?? envWsPort() ?? 37149,
|
|
36960
37358
|
host: opts.host ?? "127.0.0.1",
|
|
36961
37359
|
serverName: opts.serverName,
|
|
36962
37360
|
version: opts.version,
|
|
@@ -37016,6 +37414,8 @@ var FetchproxyServer = class {
|
|
|
37016
37414
|
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
|
|
37017
37415
|
keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
|
|
37018
37416
|
identityDir: opts.identityDir,
|
|
37417
|
+
allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
|
|
37418
|
+
requireExtensionIdentity: opts.requireExtensionIdentity,
|
|
37019
37419
|
onPairCode: opts.onPairCode
|
|
37020
37420
|
};
|
|
37021
37421
|
}
|
|
@@ -37115,7 +37515,8 @@ var FetchproxyServer = class {
|
|
|
37115
37515
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37116
37516
|
ownDomSelectors: this.opts.domSelectors,
|
|
37117
37517
|
ownGraphqlOps: this.opts.graphqlOps,
|
|
37118
|
-
onPairCode: this.opts.onPairCode
|
|
37518
|
+
onPairCode: this.opts.onPairCode,
|
|
37519
|
+
extensionTrust: this.extensionTrust()
|
|
37119
37520
|
});
|
|
37120
37521
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
37121
37522
|
this.hostHandle.onExtensionDisconnect(() => {
|
|
@@ -37144,7 +37545,9 @@ var FetchproxyServer = class {
|
|
|
37144
37545
|
localStoragePointers: this.opts.localStoragePointers,
|
|
37145
37546
|
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37146
37547
|
domSelectors: this.opts.domSelectors,
|
|
37147
|
-
graphqlOps: this.opts.graphqlOps
|
|
37548
|
+
graphqlOps: this.opts.graphqlOps,
|
|
37549
|
+
extensionTrust: this.extensionTrust(),
|
|
37550
|
+
requireExtensionIdentity: this.opts.requireExtensionIdentity
|
|
37148
37551
|
});
|
|
37149
37552
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
37150
37553
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37283,6 +37686,23 @@ var FetchproxyServer = class {
|
|
|
37283
37686
|
markActive() {
|
|
37284
37687
|
this.noteActivityForKeepalive();
|
|
37285
37688
|
}
|
|
37689
|
+
/**
|
|
37690
|
+
* #208: this MCP's pin on the extension's identity, stored beside its own
|
|
37691
|
+
* identity key and so following `identityDir` wherever the caller put it.
|
|
37692
|
+
*
|
|
37693
|
+
* `allowNewExtensionIdentity` falls back to an environment variable when the
|
|
37694
|
+
* caller expressed no opinion, because the thirteen MCPs that construct this
|
|
37695
|
+
* class are separate packages: an operator whose extension re-install has
|
|
37696
|
+
* just locked all of them out needs one lever that does not require patching
|
|
37697
|
+
* every one of them.
|
|
37698
|
+
*/
|
|
37699
|
+
extensionTrust() {
|
|
37700
|
+
return fileExtensionTrust({
|
|
37701
|
+
serverName: this.opts.serverName,
|
|
37702
|
+
dir: this.opts.identityDir,
|
|
37703
|
+
allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
|
|
37704
|
+
});
|
|
37705
|
+
}
|
|
37286
37706
|
noteActivityForKeepalive() {
|
|
37287
37707
|
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
37288
37708
|
if (intervalMs <= 0)
|
|
@@ -37346,6 +37766,7 @@ var FetchproxyServer = class {
|
|
|
37346
37766
|
this.pending.delete(id);
|
|
37347
37767
|
this.pendingReadCookies.delete(id);
|
|
37348
37768
|
this.pendingStorage.delete(id);
|
|
37769
|
+
this.pendingWriteCookies.delete(id);
|
|
37349
37770
|
this.pendingCapture.delete(id);
|
|
37350
37771
|
this.pendingRedirect.delete(id);
|
|
37351
37772
|
this.pendingDownload.delete(id);
|
|
@@ -37363,8 +37784,8 @@ var FetchproxyServer = class {
|
|
|
37363
37784
|
async _fetchOnceWithTimeout(init) {
|
|
37364
37785
|
const id = this.nextRequestId++;
|
|
37365
37786
|
const inner = { type: "request", id, op: "fetch", init };
|
|
37366
|
-
const pending = new Promise((
|
|
37367
|
-
this.pending.set(id,
|
|
37787
|
+
const pending = new Promise((resolve2) => {
|
|
37788
|
+
this.pending.set(id, resolve2);
|
|
37368
37789
|
});
|
|
37369
37790
|
await this.sendInnerFrame(inner);
|
|
37370
37791
|
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
@@ -37375,12 +37796,12 @@ var FetchproxyServer = class {
|
|
|
37375
37796
|
try {
|
|
37376
37797
|
return await Promise.race([
|
|
37377
37798
|
pending,
|
|
37378
|
-
new Promise((
|
|
37799
|
+
new Promise((resolve2) => {
|
|
37379
37800
|
timer = setTimeout(() => {
|
|
37380
37801
|
this.pending.delete(id);
|
|
37381
37802
|
const elapsedMs = Date.now() - start;
|
|
37382
37803
|
const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
|
|
37383
|
-
|
|
37804
|
+
resolve2({
|
|
37384
37805
|
ok: false,
|
|
37385
37806
|
error: error51,
|
|
37386
37807
|
kind: "timeout",
|
|
@@ -37467,7 +37888,7 @@ var FetchproxyServer = class {
|
|
|
37467
37888
|
port: this.opts.port
|
|
37468
37889
|
});
|
|
37469
37890
|
}
|
|
37470
|
-
return
|
|
37891
|
+
return protocolErrorFrom(result.error);
|
|
37471
37892
|
}
|
|
37472
37893
|
/**
|
|
37473
37894
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -37503,10 +37924,20 @@ var FetchproxyServer = class {
|
|
|
37503
37924
|
}
|
|
37504
37925
|
const url2 = isAbsolute ? path : `https://${host}${path}`;
|
|
37505
37926
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
37927
|
+
let tabUrl = `https://${host}/`;
|
|
37928
|
+
if (opts.viaTab !== void 0) {
|
|
37929
|
+
try {
|
|
37930
|
+
new URL(opts.viaTab);
|
|
37931
|
+
} catch {
|
|
37932
|
+
throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
|
|
37933
|
+
}
|
|
37934
|
+
assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
|
|
37935
|
+
tabUrl = opts.viaTab;
|
|
37936
|
+
}
|
|
37506
37937
|
const init = {
|
|
37507
37938
|
url: url2,
|
|
37508
37939
|
method,
|
|
37509
|
-
tabUrl
|
|
37940
|
+
tabUrl,
|
|
37510
37941
|
headers: opts.headers,
|
|
37511
37942
|
body: opts.body
|
|
37512
37943
|
};
|
|
@@ -37718,25 +38149,85 @@ var FetchproxyServer = class {
|
|
|
37718
38149
|
let inner;
|
|
37719
38150
|
if (opts.keys !== void 0) {
|
|
37720
38151
|
this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
|
|
38152
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
37721
38153
|
const initV3 = {
|
|
38154
|
+
// Origin stays BARE. The path travels as its own validated field —
|
|
38155
|
+
// `assertHttpsOriginOnly` deliberately refuses a path here so one
|
|
38156
|
+
// cannot be used to re-point the read past the domain gate.
|
|
37722
38157
|
origin: `https://${host}`,
|
|
37723
|
-
keys: [...opts.keys]
|
|
38158
|
+
keys: [...opts.keys],
|
|
38159
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
37724
38160
|
};
|
|
37725
38161
|
inner = { type: "request", id, op: "read_cookies", init: initV3 };
|
|
37726
38162
|
} else {
|
|
37727
38163
|
const tabUrl = `https://${host}/`;
|
|
37728
38164
|
inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
|
|
37729
38165
|
}
|
|
37730
|
-
const pending = new Promise((
|
|
37731
|
-
this.pendingReadCookies.set(id,
|
|
38166
|
+
const pending = new Promise((resolve2) => {
|
|
38167
|
+
this.pendingReadCookies.set(id, resolve2);
|
|
37732
38168
|
});
|
|
37733
38169
|
await this.sendInnerFrame(inner);
|
|
37734
38170
|
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
37735
38171
|
if (!result.ok) {
|
|
37736
|
-
throw
|
|
38172
|
+
throw protocolErrorFrom(result.error);
|
|
37737
38173
|
}
|
|
37738
38174
|
return result.cookies;
|
|
37739
38175
|
}
|
|
38176
|
+
/**
|
|
38177
|
+
* 1.12.0+: overwrite the value of cookies this MCP already declares.
|
|
38178
|
+
*
|
|
38179
|
+
* The bridge's only write verb, and it exists for one failure class. Sites
|
|
38180
|
+
* that ROTATE a credential cookie hand back a new value on every refresh; if
|
|
38181
|
+
* the MCP refreshes and keeps the result to itself, the copy in the browser's
|
|
38182
|
+
* cookie jar is dead, and the user gets signed out of a tab they never
|
|
38183
|
+
* touched — usually reported to them as "inactivity". Writing the rotated
|
|
38184
|
+
* value back is the only thing that repairs it.
|
|
38185
|
+
*
|
|
38186
|
+
* Requires `'write_cookies'` in capabilities, which the user approves at pair
|
|
38187
|
+
* time as its own line. Every name must ALSO be in declared `cookieKeys`: a
|
|
38188
|
+
* write can never reach a cookie the MCP was not already trusted to read, so
|
|
38189
|
+
* granting it cannot widen which cookies are in play — only what may be done
|
|
38190
|
+
* to the ones already listed.
|
|
38191
|
+
*
|
|
38192
|
+
* The extension refuses the whole request unless every named cookie already
|
|
38193
|
+
* exists; this refreshes a value in place and deliberately cannot author new
|
|
38194
|
+
* cookies. Returns the names actually written.
|
|
38195
|
+
*/
|
|
38196
|
+
async writeCookies(opts) {
|
|
38197
|
+
if (!this.opts.capabilities.includes("write_cookies")) {
|
|
38198
|
+
throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
38199
|
+
}
|
|
38200
|
+
const names = Object.keys(opts.cookies);
|
|
38201
|
+
if (names.length === 0) {
|
|
38202
|
+
throw new Error("FetchproxyServer.writeCookies(): no cookies given");
|
|
38203
|
+
}
|
|
38204
|
+
await this.ensureConnected();
|
|
38205
|
+
this.throwIfPendingPair();
|
|
38206
|
+
if (opts.subdomain !== void 0)
|
|
38207
|
+
assertSubdomainLabel(opts.subdomain);
|
|
38208
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
38209
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
38210
|
+
this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
|
|
38211
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
38212
|
+
const id = this.nextRequestId++;
|
|
38213
|
+
const inner = {
|
|
38214
|
+
type: "request",
|
|
38215
|
+
id,
|
|
38216
|
+
op: "write_cookies",
|
|
38217
|
+
init: {
|
|
38218
|
+
// Bare origin, same invariant as the read path: a path must never be
|
|
38219
|
+
// able to move the request past the domain gate.
|
|
38220
|
+
origin: `https://${host}`,
|
|
38221
|
+
cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
|
|
38222
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
38223
|
+
}
|
|
38224
|
+
};
|
|
38225
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38226
|
+
this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
|
|
38227
|
+
});
|
|
38228
|
+
await this.sendInnerFrame(inner);
|
|
38229
|
+
return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
|
|
38230
|
+
}
|
|
37740
38231
|
/**
|
|
37741
38232
|
* 0.3.0+: read declared localStorage keys from the user's signed-in
|
|
37742
38233
|
* tab. Requires `'read_local_storage'` in capabilities AND each key
|
|
@@ -37794,8 +38285,8 @@ var FetchproxyServer = class {
|
|
|
37794
38285
|
...opts.pointers ? { pointers: { ...opts.pointers } } : {}
|
|
37795
38286
|
}
|
|
37796
38287
|
};
|
|
37797
|
-
const pending = new Promise((
|
|
37798
|
-
this.pendingStorage.set(id, { resolve, reject });
|
|
38288
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38289
|
+
this.pendingStorage.set(id, { resolve: resolve2, reject });
|
|
37799
38290
|
});
|
|
37800
38291
|
await this.sendInnerFrame(inner);
|
|
37801
38292
|
return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
|
|
@@ -37899,8 +38390,8 @@ var FetchproxyServer = class {
|
|
|
37899
38390
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37900
38391
|
}
|
|
37901
38392
|
};
|
|
37902
|
-
const pending = new Promise((
|
|
37903
|
-
this.pendingCapture.set(id, { resolve, reject });
|
|
38393
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38394
|
+
this.pendingCapture.set(id, { resolve: resolve2, reject });
|
|
37904
38395
|
});
|
|
37905
38396
|
await this.sendInnerFrame(inner);
|
|
37906
38397
|
return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
|
|
@@ -37984,8 +38475,8 @@ var FetchproxyServer = class {
|
|
|
37984
38475
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37985
38476
|
}
|
|
37986
38477
|
};
|
|
37987
|
-
const pending = new Promise((
|
|
37988
|
-
this.pendingRedirect.set(id, { resolve, reject });
|
|
38478
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38479
|
+
this.pendingRedirect.set(id, { resolve: resolve2, reject });
|
|
37989
38480
|
});
|
|
37990
38481
|
await this.sendInnerFrame(inner);
|
|
37991
38482
|
return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
|
|
@@ -38066,8 +38557,8 @@ var FetchproxyServer = class {
|
|
|
38066
38557
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
38067
38558
|
}
|
|
38068
38559
|
};
|
|
38069
|
-
const pending = new Promise((
|
|
38070
|
-
this.pendingDownload.set(id, { resolve, reject });
|
|
38560
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38561
|
+
this.pendingDownload.set(id, { resolve: resolve2, reject });
|
|
38071
38562
|
});
|
|
38072
38563
|
await this.sendInnerFrame(inner);
|
|
38073
38564
|
return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
|
|
@@ -38114,8 +38605,8 @@ var FetchproxyServer = class {
|
|
|
38114
38605
|
keys: [...opts.keys]
|
|
38115
38606
|
}
|
|
38116
38607
|
};
|
|
38117
|
-
const pending = new Promise((
|
|
38118
|
-
this.pendingIdb.set(id, { resolve, reject });
|
|
38608
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38609
|
+
this.pendingIdb.set(id, { resolve: resolve2, reject });
|
|
38119
38610
|
});
|
|
38120
38611
|
await this.sendInnerFrame(inner);
|
|
38121
38612
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
@@ -38154,8 +38645,8 @@ var FetchproxyServer = class {
|
|
|
38154
38645
|
op: "read_dom",
|
|
38155
38646
|
init: { origin, names: [...opts.names] }
|
|
38156
38647
|
};
|
|
38157
|
-
const pending = new Promise((
|
|
38158
|
-
this.pendingStorage.set(id, { resolve, reject });
|
|
38648
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38649
|
+
this.pendingStorage.set(id, { resolve: resolve2, reject });
|
|
38159
38650
|
});
|
|
38160
38651
|
await this.sendInnerFrame(inner);
|
|
38161
38652
|
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
@@ -38200,8 +38691,8 @@ var FetchproxyServer = class {
|
|
|
38200
38691
|
...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
|
|
38201
38692
|
}
|
|
38202
38693
|
};
|
|
38203
|
-
const pending = new Promise((
|
|
38204
|
-
this.pendingGraphql.set(id, { resolve, reject });
|
|
38694
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38695
|
+
this.pendingGraphql.set(id, { resolve: resolve2, reject });
|
|
38205
38696
|
});
|
|
38206
38697
|
await this.sendInnerFrame(inner);
|
|
38207
38698
|
return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
|
|
@@ -38283,7 +38774,7 @@ var FetchproxyServer = class {
|
|
|
38283
38774
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
38284
38775
|
}
|
|
38285
38776
|
} else {
|
|
38286
|
-
storageCb.reject(
|
|
38777
|
+
storageCb.reject(protocolErrorFrom(inner.error));
|
|
38287
38778
|
}
|
|
38288
38779
|
return;
|
|
38289
38780
|
}
|
|
@@ -38297,7 +38788,7 @@ var FetchproxyServer = class {
|
|
|
38297
38788
|
captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
|
|
38298
38789
|
}
|
|
38299
38790
|
} else {
|
|
38300
|
-
captureCb.reject(
|
|
38791
|
+
captureCb.reject(protocolErrorFrom(inner.error));
|
|
38301
38792
|
}
|
|
38302
38793
|
return;
|
|
38303
38794
|
}
|
|
@@ -38311,7 +38802,7 @@ var FetchproxyServer = class {
|
|
|
38311
38802
|
redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
|
|
38312
38803
|
}
|
|
38313
38804
|
} else {
|
|
38314
|
-
redirectCb.reject(
|
|
38805
|
+
redirectCb.reject(protocolErrorFrom(inner.error));
|
|
38315
38806
|
}
|
|
38316
38807
|
return;
|
|
38317
38808
|
}
|
|
@@ -38325,7 +38816,7 @@ var FetchproxyServer = class {
|
|
|
38325
38816
|
idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
|
|
38326
38817
|
}
|
|
38327
38818
|
} else {
|
|
38328
|
-
idbCb.reject(
|
|
38819
|
+
idbCb.reject(protocolErrorFrom(inner.error));
|
|
38329
38820
|
}
|
|
38330
38821
|
return;
|
|
38331
38822
|
}
|
|
@@ -38339,7 +38830,7 @@ var FetchproxyServer = class {
|
|
|
38339
38830
|
downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
|
|
38340
38831
|
}
|
|
38341
38832
|
} else {
|
|
38342
|
-
downloadCb.reject(
|
|
38833
|
+
downloadCb.reject(protocolErrorFrom(inner.error));
|
|
38343
38834
|
}
|
|
38344
38835
|
return;
|
|
38345
38836
|
}
|
|
@@ -38353,7 +38844,17 @@ var FetchproxyServer = class {
|
|
|
38353
38844
|
graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
|
|
38354
38845
|
}
|
|
38355
38846
|
} else {
|
|
38356
|
-
graphqlCb.reject(
|
|
38847
|
+
graphqlCb.reject(protocolErrorFrom(inner.error));
|
|
38848
|
+
}
|
|
38849
|
+
return;
|
|
38850
|
+
}
|
|
38851
|
+
const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
|
|
38852
|
+
if (writeCookiesCb) {
|
|
38853
|
+
this.pendingWriteCookies.delete(inner.id);
|
|
38854
|
+
if (inner.ok && inner.op === "write_cookies") {
|
|
38855
|
+
writeCookiesCb.resolve([...inner.written]);
|
|
38856
|
+
} else {
|
|
38857
|
+
writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
|
|
38357
38858
|
}
|
|
38358
38859
|
return;
|
|
38359
38860
|
}
|
|
@@ -38396,6 +38897,9 @@ var FetchproxyServer = class {
|
|
|
38396
38897
|
for (const { reject } of this.pendingStorage.values())
|
|
38397
38898
|
reject(err);
|
|
38398
38899
|
this.pendingStorage.clear();
|
|
38900
|
+
for (const { reject } of this.pendingWriteCookies.values())
|
|
38901
|
+
reject(err);
|
|
38902
|
+
this.pendingWriteCookies.clear();
|
|
38399
38903
|
for (const { reject } of this.pendingCapture.values())
|
|
38400
38904
|
reject(err);
|
|
38401
38905
|
this.pendingCapture.clear();
|
|
@@ -38546,7 +39050,9 @@ async function runOneLift(opts) {
|
|
|
38546
39050
|
if (opts.declare.cookies.length > 0) {
|
|
38547
39051
|
const joined = await server.readCookies({
|
|
38548
39052
|
keys: opts.declare.cookies,
|
|
38549
|
-
...storageDomainOpts
|
|
39053
|
+
...storageDomainOpts,
|
|
39054
|
+
// Cookie-only: the other buckets are origin-scoped and ignore path.
|
|
39055
|
+
...opts.storagePath !== void 0 ? { path: opts.storagePath } : {}
|
|
38550
39056
|
});
|
|
38551
39057
|
for (const piece of joined.split("; ")) {
|
|
38552
39058
|
if (!piece)
|
|
@@ -38720,7 +39226,7 @@ function loadAccount(env = process.env) {
|
|
|
38720
39226
|
// package.json
|
|
38721
39227
|
var package_default = {
|
|
38722
39228
|
name: "canvas-parent-mcp",
|
|
38723
|
-
version: "1.
|
|
39229
|
+
version: "1.3.0",
|
|
38724
39230
|
mcpName: "io.github.chrischall/canvas-parent-mcp",
|
|
38725
39231
|
description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
|
|
38726
39232
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38754,19 +39260,22 @@ var package_default = {
|
|
|
38754
39260
|
".claude-plugin",
|
|
38755
39261
|
"skills",
|
|
38756
39262
|
".mcp.json",
|
|
38757
|
-
"server.json"
|
|
39263
|
+
"server.json",
|
|
39264
|
+
"mint.yaml"
|
|
38758
39265
|
],
|
|
38759
39266
|
scripts: {
|
|
38760
39267
|
build: "tsc && npm run bundle",
|
|
38761
39268
|
bundle: `esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js`,
|
|
38762
39269
|
dev: "node --env-file=.env dist/index.js",
|
|
38763
|
-
test: "vitest run",
|
|
38764
|
-
"test:
|
|
39270
|
+
test: "npm run typecheck && vitest run",
|
|
39271
|
+
"test:coverage": "npm run typecheck && vitest run --coverage",
|
|
39272
|
+
"test:watch": "vitest",
|
|
39273
|
+
typecheck: "tsc -p tsconfig.json --noEmit"
|
|
38765
39274
|
},
|
|
38766
39275
|
dependencies: {
|
|
38767
|
-
"@chrischall/mcp-utils": "^0.
|
|
38768
|
-
"@fetchproxy/bootstrap": "^
|
|
38769
|
-
"@fetchproxy/server": "^
|
|
39276
|
+
"@chrischall/mcp-utils": "^0.17.1",
|
|
39277
|
+
"@fetchproxy/bootstrap": "^2.0.0",
|
|
39278
|
+
"@fetchproxy/server": "^2.0.0",
|
|
38770
39279
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38771
39280
|
dotenv: "^17.4.2",
|
|
38772
39281
|
zod: "^4.4.3"
|
|
@@ -38873,11 +39382,128 @@ async function liftBrowserCookie(declaredDomain, baseHost) {
|
|
|
38873
39382
|
}
|
|
38874
39383
|
|
|
38875
39384
|
// src/client.ts
|
|
38876
|
-
import { writeFile as
|
|
38877
|
-
import { dirname } from "path";
|
|
39385
|
+
import { writeFile as writeFile3, stat } from "fs/promises";
|
|
38878
39386
|
|
|
38879
39387
|
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
39388
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
|
|
39389
|
+
import { dirname, join as join3, resolve } from "node:path";
|
|
39390
|
+
import { homedir as homedir2 } from "node:os";
|
|
39391
|
+
import { randomBytes, createHmac } from "node:crypto";
|
|
39392
|
+
function bindingDigest(secret, salt) {
|
|
39393
|
+
return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
|
|
39394
|
+
}
|
|
39395
|
+
function isEnvelope(raw) {
|
|
39396
|
+
return raw !== null && typeof raw === "object" && raw.v === 1 && "state" in raw;
|
|
39397
|
+
}
|
|
39398
|
+
var StatePersistenceError = class extends Error {
|
|
39399
|
+
cause;
|
|
39400
|
+
constructor(cause) {
|
|
39401
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
39402
|
+
this.name = "StatePersistenceError";
|
|
39403
|
+
this.cause = cause;
|
|
39404
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39405
|
+
}
|
|
39406
|
+
};
|
|
39407
|
+
function createFileStatePersistence(opts) {
|
|
39408
|
+
const { filePath, validate } = opts;
|
|
39409
|
+
const secret = opts.boundTo;
|
|
39410
|
+
return {
|
|
39411
|
+
load() {
|
|
39412
|
+
if (!existsSync(filePath))
|
|
39413
|
+
return null;
|
|
39414
|
+
try {
|
|
39415
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
39416
|
+
let state;
|
|
39417
|
+
if (isEnvelope(raw)) {
|
|
39418
|
+
const b = raw.boundTo;
|
|
39419
|
+
if (secret === void 0) {
|
|
39420
|
+
if (b !== void 0)
|
|
39421
|
+
return null;
|
|
39422
|
+
} else {
|
|
39423
|
+
if (b === void 0)
|
|
39424
|
+
return null;
|
|
39425
|
+
if (bindingDigest(secret, b.salt) !== b.digest)
|
|
39426
|
+
return null;
|
|
39427
|
+
}
|
|
39428
|
+
state = raw.state;
|
|
39429
|
+
} else {
|
|
39430
|
+
if (secret !== void 0)
|
|
39431
|
+
return null;
|
|
39432
|
+
state = raw;
|
|
39433
|
+
}
|
|
39434
|
+
if (validate !== void 0)
|
|
39435
|
+
return validate(state);
|
|
39436
|
+
return state;
|
|
39437
|
+
} catch {
|
|
39438
|
+
return null;
|
|
39439
|
+
}
|
|
39440
|
+
},
|
|
39441
|
+
save(state) {
|
|
39442
|
+
const dir = dirname(filePath);
|
|
39443
|
+
const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
|
|
39444
|
+
const dirExisted = existsSync(dir);
|
|
39445
|
+
try {
|
|
39446
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
39447
|
+
if (!dirExisted)
|
|
39448
|
+
chmodSync(dir, 448);
|
|
39449
|
+
let boundTo;
|
|
39450
|
+
if (secret !== void 0) {
|
|
39451
|
+
const salt = randomBytes(16).toString("hex");
|
|
39452
|
+
boundTo = { salt, digest: bindingDigest(secret, salt) };
|
|
39453
|
+
}
|
|
39454
|
+
const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
|
|
39455
|
+
writeFileSync(tmp, JSON.stringify(envelope, null, 2), { mode: 384 });
|
|
39456
|
+
chmodSync(tmp, 384);
|
|
39457
|
+
renameSync(tmp, filePath);
|
|
39458
|
+
chmodSync(filePath, 384);
|
|
39459
|
+
} catch (err) {
|
|
39460
|
+
try {
|
|
39461
|
+
if (existsSync(tmp))
|
|
39462
|
+
unlinkSync(tmp);
|
|
39463
|
+
} catch {
|
|
39464
|
+
}
|
|
39465
|
+
throw err;
|
|
39466
|
+
}
|
|
39467
|
+
},
|
|
39468
|
+
clear() {
|
|
39469
|
+
try {
|
|
39470
|
+
if (existsSync(filePath))
|
|
39471
|
+
unlinkSync(filePath);
|
|
39472
|
+
} catch {
|
|
39473
|
+
}
|
|
39474
|
+
}
|
|
39475
|
+
};
|
|
39476
|
+
}
|
|
39477
|
+
function homeOf(env) {
|
|
39478
|
+
return readEnvVar("HOME", { env }) ?? homedir2();
|
|
39479
|
+
}
|
|
39480
|
+
function resolveStateDir(opts = {}) {
|
|
39481
|
+
const env = opts.env;
|
|
39482
|
+
return join3(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
|
|
39483
|
+
}
|
|
39484
|
+
function resolveStateFile(opts) {
|
|
39485
|
+
const override = opts.envVar !== void 0 ? readEnvVar(opts.envVar, { env: opts.env }) : void 0;
|
|
39486
|
+
if (override !== void 0) {
|
|
39487
|
+
if (override === "~")
|
|
39488
|
+
return homeOf(opts.env);
|
|
39489
|
+
if (override.startsWith("~/"))
|
|
39490
|
+
return join3(homeOf(opts.env), override.slice(2));
|
|
39491
|
+
return resolve(override);
|
|
39492
|
+
}
|
|
39493
|
+
const dirOpts = { env: opts.env };
|
|
39494
|
+
if (opts.subdir !== void 0)
|
|
39495
|
+
dirOpts.subdir = opts.subdir;
|
|
39496
|
+
return join3(resolveStateDir(dirOpts), opts.fileName);
|
|
39497
|
+
}
|
|
38880
39498
|
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
39499
|
+
function isPersistedCookieSession(raw) {
|
|
39500
|
+
if (raw === null || typeof raw !== "object")
|
|
39501
|
+
return false;
|
|
39502
|
+
const r = raw;
|
|
39503
|
+
if (typeof r.sessionAt !== "number" || !Number.isFinite(r.sessionAt))
|
|
39504
|
+
return false;
|
|
39505
|
+
return typeof r.session === "object" && r.session !== null;
|
|
39506
|
+
}
|
|
38881
39507
|
var CookieSessionManager = class {
|
|
38882
39508
|
session;
|
|
38883
39509
|
inFlight;
|
|
@@ -38891,6 +39517,17 @@ var CookieSessionManager = class {
|
|
|
38891
39517
|
maxAgeMs;
|
|
38892
39518
|
now;
|
|
38893
39519
|
onReplayLoginErrorFn;
|
|
39520
|
+
persistence;
|
|
39521
|
+
onPersistErrorFn;
|
|
39522
|
+
/** Persistence is consulted once per process; a miss must not be re-read. */
|
|
39523
|
+
persistenceRead = false;
|
|
39524
|
+
/**
|
|
39525
|
+
* Serializes persistence writes. `seed()` and `invalidate()` are synchronous
|
|
39526
|
+
* by contract and so fire-and-forget their save/clear; with an async backend a
|
|
39527
|
+
* slow save could otherwise land AFTER the clear that followed it and leave an
|
|
39528
|
+
* invalidated session on disk.
|
|
39529
|
+
*/
|
|
39530
|
+
persistChain = Promise.resolve();
|
|
38894
39531
|
constructor(opts) {
|
|
38895
39532
|
this.loginFn = opts.login;
|
|
38896
39533
|
this.isExpiredFn = opts.isExpired ?? (() => false);
|
|
@@ -38898,6 +39535,8 @@ var CookieSessionManager = class {
|
|
|
38898
39535
|
this.maxAgeMs = opts.maxAgeMs;
|
|
38899
39536
|
this.now = opts.now ?? Date.now;
|
|
38900
39537
|
this.onReplayLoginErrorFn = opts.onReplayLoginError;
|
|
39538
|
+
this.persistence = opts.persistence;
|
|
39539
|
+
this.onPersistErrorFn = opts.onPersistError;
|
|
38901
39540
|
}
|
|
38902
39541
|
/** The current session, or `undefined` before the first successful login. */
|
|
38903
39542
|
get current() {
|
|
@@ -38946,6 +39585,8 @@ var CookieSessionManager = class {
|
|
|
38946
39585
|
this.session = session;
|
|
38947
39586
|
this.sessionAt = this.now();
|
|
38948
39587
|
this.inFlight = void 0;
|
|
39588
|
+
this.persistenceRead = true;
|
|
39589
|
+
void this.persist(session, this.sessionAt);
|
|
38949
39590
|
}
|
|
38950
39591
|
/**
|
|
38951
39592
|
* One login attempt. Self-clears `inFlight` on settle so a rejected login
|
|
@@ -38957,13 +39598,25 @@ var CookieSessionManager = class {
|
|
|
38957
39598
|
const holder = {};
|
|
38958
39599
|
holder.p = (async () => {
|
|
38959
39600
|
try {
|
|
39601
|
+
const restored = this.persistence !== void 0 && !this.persistenceRead ? await this.restoreFromPersistence() : null;
|
|
39602
|
+
if (restored !== null) {
|
|
39603
|
+
if (this.inFlight === holder.p) {
|
|
39604
|
+
this.session = restored.session;
|
|
39605
|
+
this.sessionAt = restored.sessionAt;
|
|
39606
|
+
}
|
|
39607
|
+
return restored.session;
|
|
39608
|
+
}
|
|
38960
39609
|
const session = await this.loginFn();
|
|
39610
|
+
const at = this.now();
|
|
38961
39611
|
if (this.inFlight === holder.p) {
|
|
38962
39612
|
this.session = session;
|
|
38963
|
-
this.sessionAt =
|
|
39613
|
+
this.sessionAt = at;
|
|
38964
39614
|
}
|
|
39615
|
+
await this.persist(session, at);
|
|
38965
39616
|
return session;
|
|
38966
39617
|
} catch (err) {
|
|
39618
|
+
if (err instanceof StatePersistenceError)
|
|
39619
|
+
throw err.cause;
|
|
38967
39620
|
if (this.isPermanentErrorFn(err))
|
|
38968
39621
|
this.permanentError = err;
|
|
38969
39622
|
throw err;
|
|
@@ -38983,6 +39636,71 @@ var CookieSessionManager = class {
|
|
|
38983
39636
|
invalidate() {
|
|
38984
39637
|
this.session = void 0;
|
|
38985
39638
|
this.inFlight = void 0;
|
|
39639
|
+
void this.clearPersisted();
|
|
39640
|
+
}
|
|
39641
|
+
/**
|
|
39642
|
+
* The persisted session, if there is one worth using. Read at most once per
|
|
39643
|
+
* process — after that the in-memory session (or its absence) is the truth,
|
|
39644
|
+
* so an invalidate() cannot be undone by a stale file.
|
|
39645
|
+
*/
|
|
39646
|
+
async restoreFromPersistence() {
|
|
39647
|
+
if (this.persistence === void 0 || this.persistenceRead)
|
|
39648
|
+
return null;
|
|
39649
|
+
this.persistenceRead = true;
|
|
39650
|
+
try {
|
|
39651
|
+
let raw = null;
|
|
39652
|
+
await this.enqueuePersist(async () => {
|
|
39653
|
+
raw = await this.persistence?.load();
|
|
39654
|
+
});
|
|
39655
|
+
if (!isPersistedCookieSession(raw))
|
|
39656
|
+
return null;
|
|
39657
|
+
if (this.maxAgeMs !== void 0 && this.now() - raw.sessionAt >= this.maxAgeMs)
|
|
39658
|
+
return null;
|
|
39659
|
+
return raw;
|
|
39660
|
+
} catch {
|
|
39661
|
+
return null;
|
|
39662
|
+
}
|
|
39663
|
+
}
|
|
39664
|
+
/**
|
|
39665
|
+
* Append a persistence op to the chain, preserving call order.
|
|
39666
|
+
*
|
|
39667
|
+
* The RETURNED promise can reject — that is the whole `StatePersistenceError`
|
|
39668
|
+
* path, and the awaited login write depends on it. Only the retained chain is
|
|
39669
|
+
* swallowed, so one failed write cannot poison every later one.
|
|
39670
|
+
*/
|
|
39671
|
+
enqueuePersist(op) {
|
|
39672
|
+
const next = this.persistChain.then(op);
|
|
39673
|
+
this.persistChain = next.catch(() => void 0);
|
|
39674
|
+
return next;
|
|
39675
|
+
}
|
|
39676
|
+
/** Write the session. Silent unless `onPersistError` throws. */
|
|
39677
|
+
persist(session, sessionAt) {
|
|
39678
|
+
return this.enqueuePersist(async () => {
|
|
39679
|
+
if (this.persistence === void 0)
|
|
39680
|
+
return;
|
|
39681
|
+
try {
|
|
39682
|
+
await this.persistence.save({ session, sessionAt });
|
|
39683
|
+
} catch (err) {
|
|
39684
|
+
if (this.onPersistErrorFn === void 0)
|
|
39685
|
+
return;
|
|
39686
|
+
try {
|
|
39687
|
+
this.onPersistErrorFn(err);
|
|
39688
|
+
} catch (hookErr) {
|
|
39689
|
+
throw new StatePersistenceError(hookErr);
|
|
39690
|
+
}
|
|
39691
|
+
}
|
|
39692
|
+
});
|
|
39693
|
+
}
|
|
39694
|
+
/** Discard the persisted session. Never throws. */
|
|
39695
|
+
clearPersisted() {
|
|
39696
|
+
return this.enqueuePersist(async () => {
|
|
39697
|
+
if (this.persistence?.clear === void 0)
|
|
39698
|
+
return;
|
|
39699
|
+
try {
|
|
39700
|
+
await this.persistence.clear();
|
|
39701
|
+
} catch {
|
|
39702
|
+
}
|
|
39703
|
+
});
|
|
38986
39704
|
}
|
|
38987
39705
|
/**
|
|
38988
39706
|
* Run an authenticated `call` with the current session and reactive
|
|
@@ -39016,6 +39734,57 @@ var CookieSessionManager = class {
|
|
|
39016
39734
|
}
|
|
39017
39735
|
};
|
|
39018
39736
|
|
|
39737
|
+
// src/session-cache.ts
|
|
39738
|
+
function sessionCachePath(env = process.env) {
|
|
39739
|
+
return resolveStateFile({
|
|
39740
|
+
env,
|
|
39741
|
+
envVar: "CANVAS_SESSION_FILE",
|
|
39742
|
+
subdir: ".canvas-parent-mcp",
|
|
39743
|
+
fileName: "session.json"
|
|
39744
|
+
});
|
|
39745
|
+
}
|
|
39746
|
+
function isRecord(raw) {
|
|
39747
|
+
if (raw === null || typeof raw !== "object") return false;
|
|
39748
|
+
const r = raw;
|
|
39749
|
+
if (typeof r.sessionAt !== "number") return false;
|
|
39750
|
+
const s = r.session;
|
|
39751
|
+
if (s === null || typeof s !== "object") return false;
|
|
39752
|
+
const h = s.headers;
|
|
39753
|
+
if (h === null || typeof h !== "object" || Object.keys(h).length === 0) return false;
|
|
39754
|
+
if (Object.values(h).some((v) => typeof v !== "string")) return false;
|
|
39755
|
+
return s.accessTokenExpiresAt === void 0 || typeof s.accessTokenExpiresAt === "number";
|
|
39756
|
+
}
|
|
39757
|
+
function bindingFor(account, browserBacked) {
|
|
39758
|
+
if (account.mode === "token") return null;
|
|
39759
|
+
if (account.mode === "session") {
|
|
39760
|
+
if (browserBacked) return null;
|
|
39761
|
+
return ["session", account.baseUrl, account.username.trim().toLowerCase(), account.password].join(
|
|
39762
|
+
"\0"
|
|
39763
|
+
);
|
|
39764
|
+
}
|
|
39765
|
+
return ["oauth", account.baseUrl, account.clientId, account.refreshToken].join("\0");
|
|
39766
|
+
}
|
|
39767
|
+
function createSessionCache(account, opts = {}) {
|
|
39768
|
+
const env = opts.env ?? process.env;
|
|
39769
|
+
if (!parseBoolEnv("CANVAS_SESSION_CACHE", { env, default: true })) return null;
|
|
39770
|
+
const boundTo = bindingFor(account, opts.browserBacked === true);
|
|
39771
|
+
if (boundTo === null) return null;
|
|
39772
|
+
return createFileStatePersistence({
|
|
39773
|
+
filePath: sessionCachePath(env),
|
|
39774
|
+
boundTo,
|
|
39775
|
+
validate: (raw) => isRecord(raw) ? raw : null
|
|
39776
|
+
});
|
|
39777
|
+
}
|
|
39778
|
+
function reportCacheWriteFailure(err) {
|
|
39779
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
39780
|
+
console.error(
|
|
39781
|
+
`[canvas-parent-mcp] could not cache the session (${detail}); continuing without the cache \u2014 every restart will authenticate again until this is fixed.`
|
|
39782
|
+
);
|
|
39783
|
+
}
|
|
39784
|
+
|
|
39785
|
+
// src/client.ts
|
|
39786
|
+
import { dirname as dirname2 } from "path";
|
|
39787
|
+
|
|
39019
39788
|
// src/session-login.ts
|
|
39020
39789
|
var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
|
|
39021
39790
|
var SessionLoginError = class extends Error {
|
|
@@ -39136,7 +39905,9 @@ var CanvasClient = class {
|
|
|
39136
39905
|
// account with neither a lift nor credentials still can't — those 401s
|
|
39137
39906
|
// fall through as a Response and `mapStatus` turns them into a
|
|
39138
39907
|
// TokenExpiredError.
|
|
39139
|
-
isExpired: (res) => res.status === 401 && this.canReauth()
|
|
39908
|
+
isExpired: (res) => res.status === 401 && this.canReauth(),
|
|
39909
|
+
persistence: createSessionCache(account, { browserBacked: this.refreshSession !== null }) ?? void 0,
|
|
39910
|
+
onPersistError: reportCacheWriteFailure
|
|
39140
39911
|
});
|
|
39141
39912
|
}
|
|
39142
39913
|
/** Account metadata (no secrets) — useful for diagnostics. */
|
|
@@ -39174,7 +39945,7 @@ var CanvasClient = class {
|
|
|
39174
39945
|
}
|
|
39175
39946
|
if (destStat?.isDirectory()) throw new InvalidPathError(destinationPath);
|
|
39176
39947
|
if (destStat && !opts.overwrite) throw new FileExistsError(destinationPath);
|
|
39177
|
-
const parent =
|
|
39948
|
+
const parent = dirname2(destinationPath);
|
|
39178
39949
|
try {
|
|
39179
39950
|
await stat(parent);
|
|
39180
39951
|
} catch {
|
|
@@ -39186,7 +39957,7 @@ var CanvasClient = class {
|
|
|
39186
39957
|
if (res.status === 404) throw new Error(`Canvas download 404 for ${path}`);
|
|
39187
39958
|
if (!res.ok) throw new Error(`Canvas download ${res.status} for ${path}`);
|
|
39188
39959
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
39189
|
-
await
|
|
39960
|
+
await writeFile3(destinationPath, buf);
|
|
39190
39961
|
return {
|
|
39191
39962
|
path: destinationPath,
|
|
39192
39963
|
bytes: buf.byteLength,
|
|
@@ -39709,8 +40480,8 @@ function registerFileTools(server, client) {
|
|
|
39709
40480
|
}
|
|
39710
40481
|
|
|
39711
40482
|
// src/index.ts
|
|
39712
|
-
var __dirname =
|
|
39713
|
-
await loadDotenvSafely({ path:
|
|
40483
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
40484
|
+
await loadDotenvSafely({ path: join4(__dirname, "..", ".env"), override: false });
|
|
39714
40485
|
var resolved = null;
|
|
39715
40486
|
var configError = null;
|
|
39716
40487
|
try {
|
|
@@ -39720,7 +40491,7 @@ try {
|
|
|
39720
40491
|
}
|
|
39721
40492
|
await runMcp({
|
|
39722
40493
|
name: "canvas",
|
|
39723
|
-
version: "1.
|
|
40494
|
+
version: "1.3.0",
|
|
39724
40495
|
// x-release-please-version
|
|
39725
40496
|
tools: [
|
|
39726
40497
|
(server) => {
|