@xxxyz/dsh-mcp-manager 2.0.5 → 2.0.6

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/install.mjs CHANGED
@@ -273,7 +273,7 @@ async function pollApi(port, timeoutMs) {
273
273
  try {
274
274
  const res = await fetch(url, {
275
275
  method: 'POST',
276
- headers: { 'content-type': 'application/json' },
276
+ headers: { 'content-type': 'application/json', 'x-dsh-plugin': 'dsh-mcp-manager' },
277
277
  body: JSON.stringify({ op: 'mcpm-list', args: {} }),
278
278
  signal: timeoutSignal(2000),
279
279
  })
package/lib/client.js CHANGED
@@ -61,9 +61,12 @@ const factory = (require) => {
61
61
  }
62
62
 
63
63
  function apiCall(op, args) {
64
+ // `x-dsh-plugin` is the cross-site (CSRF) gate header the host half
65
+ // requires on every request; a cross-origin page cannot attach it
66
+ // without a CORS preflight that this route never answers.
64
67
  return fetch('/dsh-mcp-manager/api', {
65
68
  method: 'POST',
66
- headers: { 'content-type': 'application/json' },
69
+ headers: { 'content-type': 'application/json', 'x-dsh-plugin': 'dsh-mcp-manager' },
67
70
  body: JSON.stringify({ op, args: args || {} }),
68
71
  }).then((r) => r.json()).catch((e) => ({ ok: false, error: String((e && e.message) || e) }))
69
72
  }
package/lib/index.js CHANGED
@@ -936,6 +936,42 @@ export default {
936
936
  kind: 'exact',
937
937
  path: '/dsh-mcp-manager/api',
938
938
  handler: async (req, res) => {
939
+ // Cross-site (CSRF) gate. This route mutates config files, so it
940
+ // must only be reachable from the DSH web UI (same origin) or
941
+ // local tooling. A browser cross-site request cannot attach a
942
+ // custom header without a CORS preflight, and this route never
943
+ // answers preflights — requiring `x-dsh-plugin` is the primary
944
+ // gate; POST-only and the Origin check are defense in depth.
945
+ const hdr = (name) => {
946
+ const v = req.headers?.[name];
947
+ return Array.isArray(v) ? v[0] ?? '' : v ?? '';
948
+ };
949
+ if (String(req.method || 'POST').toUpperCase() !== 'POST') {
950
+ res.writeHead(405, { 'content-type': 'application/json' });
951
+ res.end(JSON.stringify({ ok: false, error: 'method not allowed' }));
952
+ return;
953
+ }
954
+ if (hdr('x-dsh-plugin') !== 'dsh-mcp-manager') {
955
+ res.writeHead(403, { 'content-type': 'application/json' });
956
+ res.end(JSON.stringify({ ok: false, error: 'missing plugin gate header' }));
957
+ return;
958
+ }
959
+ const origin = hdr('origin');
960
+ if (origin) {
961
+ let sameOrigin = false;
962
+ try {
963
+ const u = new URL(origin);
964
+ const hostHdr = hdr('host') || '';
965
+ sameOrigin =
966
+ /^(localhost|127\.0\.0\.1|\[::1\])$/i.test(u.hostname) || u.host === hostHdr;
967
+ }
968
+ catch (e) { /* unparseable origin → rejected below */ }
969
+ if (!sameOrigin) {
970
+ res.writeHead(403, { 'content-type': 'application/json' });
971
+ res.end(JSON.stringify({ ok: false, error: 'cross-origin request rejected' }));
972
+ return;
973
+ }
974
+ }
939
975
  res.writeHead(200, { 'content-type': 'application/json' });
940
976
  try {
941
977
  let payload = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xxxyz/dsh-mcp-manager",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "DSH-standard MCP manager plugin: Settings UI + HTTP API + model-facing mcp_manager_* tools. Composed as a loader entry, survives restarts. Install via npx, PowerShell, bash or node.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",