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/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 = resolve.call(this, root, ref);
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 resolve(root, ref) {
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 resolve(baseURI, relativeURI, options) {
3652
+ function resolve2(baseURI, relativeURI, options) {
3653
3653
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3654
- const resolved2 = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
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 = randomBytes(16).toString("base64");
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 dirname2, join as join2 } from "path";
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((resolve) => setTimeout(resolve, pollInterval));
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((resolve, reject) => {
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
- resolve(parseResult.data);
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((resolve, reject) => {
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(resolve, interval);
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((resolve) => setTimeout(resolve, pollInterval));
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((resolve) => {
34673
+ return new Promise((resolve2) => {
34654
34674
  const json2 = serializeMessage(message);
34655
34675
  if (this._stdout.write(json2)) {
34656
- resolve();
34676
+ resolve2();
34657
34677
  } else {
34658
- this._stdout.once("drain", resolve);
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 = 2;
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((resolve, reject) => {
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
- resolve({ role: "peer" });
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
- resolve({ role: "host", server });
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((resolve, reject) => {
36344
- resolveOwnSession = resolve;
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 = concatBytes(ownSessionNonce, extNonce);
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((resolve) => {
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(() => resolve());
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((resolve, reject) => {
36549
- ws.once("open", () => resolve());
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((resolve, reject) => {
36580
- resolveFirstReady = resolve;
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((resolve) => {
37367
- this.pending.set(id, resolve);
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((resolve) => {
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
- resolve({
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 new FetchproxyProtocolError(result.error);
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: `https://${host}/`,
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((resolve) => {
37731
- this.pendingReadCookies.set(id, resolve);
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 new FetchproxyProtocolError(result.error);
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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(new FetchproxyProtocolError(inner.error));
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(new FetchproxyProtocolError(inner.error));
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(new FetchproxyProtocolError(inner.error));
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(new FetchproxyProtocolError(inner.error));
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(new FetchproxyProtocolError(inner.error));
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(new FetchproxyProtocolError(inner.error));
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.2.4",
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:watch": "vitest"
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.14.0",
38768
- "@fetchproxy/bootstrap": "^1.9.1",
38769
- "@fetchproxy/server": "^1.7.0",
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 writeFile2, stat } from "fs/promises";
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 = this.now();
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 = dirname(destinationPath);
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 writeFile2(destinationPath, buf);
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 = dirname2(fileURLToPath(import.meta.url));
39713
- await loadDotenvSafely({ path: join2(__dirname, "..", ".env"), override: false });
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.2.4",
40494
+ version: "1.3.0",
39724
40495
  // x-release-please-version
39725
40496
  tools: [
39726
40497
  (server) => {