@trops/dash-core 0.1.486 → 0.1.489

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.
@@ -4,7 +4,7 @@ var require$$0$1 = require('electron');
4
4
  var require$$1$1 = require('electron-store');
5
5
  var require$$1$2 = require('path');
6
6
  var require$$0$2 = require('fs');
7
- var require$$5$1 = require('objects-to-csv');
7
+ var require$$6$1 = require('objects-to-csv');
8
8
  var require$$1$3 = require('readline');
9
9
  var require$$2 = require('xtreamer');
10
10
  var require$$3$1 = require('xml2js');
@@ -12,12 +12,12 @@ var require$$4 = require('JSONStream');
12
12
  var require$$5 = require('stream');
13
13
  var require$$6 = require('csv-parser');
14
14
  var require$$0$3 = require('quickjs-emscripten');
15
- var require$$7 = require('https');
15
+ var require$$8$1 = require('https');
16
16
  var require$$0$5 = require('@modelcontextprotocol/sdk/client/index.js');
17
17
  var require$$1$4 = require('@modelcontextprotocol/sdk/client/stdio.js');
18
18
  var require$$0$4 = require('pkce-challenge');
19
19
  var require$$2$1 = require('os');
20
- var require$$7$1 = require('child_process');
20
+ var require$$9$1 = require('child_process');
21
21
  var require$$3$2 = require('adm-zip');
22
22
  var require$$4$1 = require('url');
23
23
  var require$$2$2 = require('vm');
@@ -28,7 +28,7 @@ var require$$0$6 = require('openai');
28
28
  require('live-plugin-manager');
29
29
  var require$$0$9 = require('@anthropic-ai/sdk');
30
30
  var require$$3$4 = require('crypto');
31
- var require$$8$1 = require('zod');
31
+ var require$$8$2 = require('zod');
32
32
  var require$$0$7 = require('http');
33
33
  var require$$1$6 = require('http2');
34
34
  var require$$2$4 = require('node-forge');
@@ -1107,7 +1107,7 @@ var secureStoreController$1 = {
1107
1107
  getData: getData$1,
1108
1108
  };
1109
1109
 
1110
- const path$j = require$$1$2;
1110
+ const path$l = require$$1$2;
1111
1111
  const {
1112
1112
  readFileSync,
1113
1113
  writeFileSync: writeFileSync$4,
@@ -1125,7 +1125,7 @@ const {
1125
1125
  function ensureDirectoryExistence$2(filePath) {
1126
1126
  try {
1127
1127
  // isDirectory
1128
- var dirname = path$j.dirname(filePath);
1128
+ var dirname = path$l.dirname(filePath);
1129
1129
  // check if the directory exists...return true
1130
1130
  // if not, we can pass in the dirname as the filepath
1131
1131
  // and check each directory recursively.
@@ -1240,7 +1240,7 @@ function removeFilesFromDirectory(directory, excludeFiles = []) {
1240
1240
 
1241
1241
  for (const file of files) {
1242
1242
  if (!excludeFiles.includes(file)) {
1243
- unlinkSync(path$j.join(directory, file), (err) => {
1243
+ unlinkSync(path$l.join(directory, file), (err) => {
1244
1244
  if (err) throw err;
1245
1245
  });
1246
1246
  }
@@ -1257,8 +1257,8 @@ var file = {
1257
1257
  checkDirectory: checkDirectory$1,
1258
1258
  };
1259
1259
 
1260
- const { app: app$b } = require$$0$1;
1261
- const path$i = require$$1$2;
1260
+ const { app: app$e } = require$$0$1;
1261
+ const path$k = require$$1$2;
1262
1262
  const { writeFileSync: writeFileSync$3 } = require$$0$2;
1263
1263
  const { getFileContents: getFileContents$7 } = file;
1264
1264
 
@@ -1305,8 +1305,8 @@ const workspaceController$3 = {
1305
1305
  saveWorkspaceForApplication: (win, appId, workspaceObject) => {
1306
1306
  try {
1307
1307
  // filename to the pages file (live pages)
1308
- const filename = path$i.join(
1309
- app$b.getPath("userData"),
1308
+ const filename = path$k.join(
1309
+ app$e.getPath("userData"),
1310
1310
  appName$7,
1311
1311
  appId,
1312
1312
  configFilename$5,
@@ -1354,8 +1354,8 @@ const workspaceController$3 = {
1354
1354
  saveMenuItemsForApplication: (win, appId, menuItems) => {
1355
1355
  try {
1356
1356
  // filename to the workspaces file
1357
- const filename = path$i.join(
1358
- app$b.getPath("userData"),
1357
+ const filename = path$k.join(
1358
+ app$e.getPath("userData"),
1359
1359
  appName$7,
1360
1360
  appId,
1361
1361
  configFilename$5,
@@ -1403,8 +1403,8 @@ const workspaceController$3 = {
1403
1403
  */
1404
1404
  deleteWorkspaceForApplication: (win, appId, workspaceId) => {
1405
1405
  try {
1406
- const filename = path$i.join(
1407
- app$b.getPath("userData"),
1406
+ const filename = path$k.join(
1407
+ app$e.getPath("userData"),
1408
1408
  appName$7,
1409
1409
  appId,
1410
1410
  configFilename$5,
@@ -1437,8 +1437,8 @@ const workspaceController$3 = {
1437
1437
 
1438
1438
  listWorkspacesForApplication: (win, appId) => {
1439
1439
  try {
1440
- const filename = path$i.join(
1441
- app$b.getPath("userData"),
1440
+ const filename = path$k.join(
1441
+ app$e.getPath("userData"),
1442
1442
  appName$7,
1443
1443
  appId,
1444
1444
  configFilename$5,
@@ -1465,8 +1465,8 @@ const workspaceController$3 = {
1465
1465
 
1466
1466
  listMenuItemsForApplication: (win, appId) => {
1467
1467
  try {
1468
- const filename = path$i.join(
1469
- app$b.getPath("userData"),
1468
+ const filename = path$k.join(
1469
+ app$e.getPath("userData"),
1470
1470
  appName$7,
1471
1471
  appId,
1472
1472
  configFilename$5,
@@ -1509,8 +1509,8 @@ const workspaceController$3 = {
1509
1509
 
1510
1510
  var workspaceController_1 = workspaceController$3;
1511
1511
 
1512
- const { app: app$a } = require$$0$1;
1513
- const path$h = require$$1$2;
1512
+ const { app: app$d } = require$$0$1;
1513
+ const path$j = require$$1$2;
1514
1514
  const { writeFileSync: writeFileSync$2 } = require$$0$2;
1515
1515
  const { getFileContents: getFileContents$6 } = file;
1516
1516
 
@@ -1530,8 +1530,8 @@ const themeController$5 = {
1530
1530
  saveThemeForApplication: (win, appId, name, obj) => {
1531
1531
  try {
1532
1532
  // filename to the pages file (live pages)
1533
- const filename = path$h.join(
1534
- app$a.getPath("userData"),
1533
+ const filename = path$j.join(
1534
+ app$d.getPath("userData"),
1535
1535
  appName$6,
1536
1536
  appId,
1537
1537
  configFilename$4,
@@ -1576,8 +1576,8 @@ const themeController$5 = {
1576
1576
  */
1577
1577
  listThemesForApplication: (win, appId) => {
1578
1578
  try {
1579
- const filename = path$h.join(
1580
- app$a.getPath("userData"),
1579
+ const filename = path$j.join(
1580
+ app$d.getPath("userData"),
1581
1581
  appName$6,
1582
1582
  appId,
1583
1583
  configFilename$4,
@@ -1618,8 +1618,8 @@ const themeController$5 = {
1618
1618
  */
1619
1619
  deleteThemeForApplication: (win, appId, themeKey) => {
1620
1620
  try {
1621
- const filename = path$h.join(
1622
- app$a.getPath("userData"),
1621
+ const filename = path$j.join(
1622
+ app$d.getPath("userData"),
1623
1623
  appName$6,
1624
1624
  appId,
1625
1625
  configFilename$4,
@@ -1650,6 +1650,179 @@ const themeController$5 = {
1650
1650
 
1651
1651
  var themeController_1 = themeController$5;
1652
1652
 
1653
+ /**
1654
+ * safePath.js
1655
+ *
1656
+ * Path-traversal containment for IPC handlers that accept renderer-
1657
+ * supplied paths.
1658
+ *
1659
+ * Why: dash-core exposes IPC handlers (mainApi.data.saveData,
1660
+ * mainApi.data.parseXMLStream, mainApi.algolia.createBatchesFromFile,
1661
+ * etc.) that historically passed renderer-controlled paths directly to
1662
+ * fs.writeFileSync / fs.createReadStream. A widget could pass
1663
+ * "../../etc/passwd" and the handler would write/read OUTSIDE the
1664
+ * intended app data directory because path.join doesn't reject `..`
1665
+ * segments. See docs/security/ipc-filesystem-audit.md for the full
1666
+ * finding set.
1667
+ *
1668
+ * This utility resolves the requested path, walks symlinks, and asserts
1669
+ * containment within at least one explicitly-allowed root. Any handler
1670
+ * that takes a renderer path runs it through `safePath(p, roots)` and
1671
+ * either gets back a validated absolute real-path, or throws.
1672
+ *
1673
+ * Public API:
1674
+ *
1675
+ * safePath(requested, allowedRoots[]) → string
1676
+ * Throws on traversal, missing input, or empty roots. Returns the
1677
+ * resolved real-path (which is what the caller should pass to fs).
1678
+ *
1679
+ * getAllowedRoots(category) → string[]
1680
+ * Canonical roots per category. Categories:
1681
+ * "data" — userData/Dashboard/data + user-configured override
1682
+ * "themes" — userData/Dashboard/themes
1683
+ * "widgets" — userData/widgets
1684
+ * "plugins" — userData/plugins
1685
+ * "downloads"— OS Downloads folder
1686
+ *
1687
+ * Defense layers:
1688
+ * 1. path.resolve() to absolute form.
1689
+ * 2. fs.realpathSync() through any symlinks. If the path doesn't
1690
+ * exist yet, realpath the parent directory (so a symlink-in-parent
1691
+ * can't trick a future create operation).
1692
+ * 3. startsWith(realRoot + path.sep) test — single-equals check
1693
+ * handles "exactly the root" case, prefix-with-sep handles
1694
+ * "inside the root" without false-matching `/data-evil/` against
1695
+ * `/data/`.
1696
+ */
1697
+
1698
+ const path$i = require$$1$2;
1699
+ const fs$d = require$$0$2;
1700
+ const { app: app$c } = require$$0$1;
1701
+
1702
+ const APP_NAME = "Dashboard";
1703
+
1704
+ /**
1705
+ * @param {string} category
1706
+ * @returns {string[]} ordered allowed roots for that category
1707
+ */
1708
+ function getAllowedRoots$2(category) {
1709
+ const userData = app$c.getPath("userData");
1710
+ switch (category) {
1711
+ case "data": {
1712
+ const def = path$i.join(userData, APP_NAME, "data");
1713
+ // The user can configure a custom data directory in
1714
+ // Settings → General → Data Directory. If set, that
1715
+ // location is ALSO an allowed root. We don't replace the
1716
+ // default — both are valid because legacy data may still
1717
+ // live in the default while new data goes to the override.
1718
+ const override = readDataDirectoryFromSettings();
1719
+ return override ? [def, override] : [def];
1720
+ }
1721
+ case "themes":
1722
+ return [path$i.join(userData, APP_NAME, "themes")];
1723
+ case "widgets":
1724
+ return [path$i.join(userData, "widgets")];
1725
+ case "plugins":
1726
+ return [path$i.join(userData, "plugins")];
1727
+ case "downloads":
1728
+ return [app$c.getPath("downloads")];
1729
+ default:
1730
+ throw new Error("safePath: unknown allowed-roots category: " + category);
1731
+ }
1732
+ }
1733
+
1734
+ /**
1735
+ * Read the user-configured data directory from settings.json. Returns
1736
+ * undefined if not set or unreadable.
1737
+ *
1738
+ * Inlined to avoid a circular require with settingsController. Reads
1739
+ * the same settings.json file directly.
1740
+ */
1741
+ function readDataDirectoryFromSettings() {
1742
+ try {
1743
+ const settingsPath = path$i.join(
1744
+ app$c.getPath("userData"),
1745
+ APP_NAME,
1746
+ "settings.json",
1747
+ );
1748
+ if (!fs$d.existsSync(settingsPath)) return undefined;
1749
+ const raw = fs$d.readFileSync(settingsPath, "utf8");
1750
+ const settings = JSON.parse(raw);
1751
+ const dir = settings && settings.dataDirectory;
1752
+ if (typeof dir === "string" && dir) return dir;
1753
+ } catch (_e) {
1754
+ // best-effort — fall through to default
1755
+ }
1756
+ return undefined;
1757
+ }
1758
+
1759
+ /**
1760
+ * Resolve and validate a path against allowed roots.
1761
+ *
1762
+ * @param {string} requested the path the renderer asked for
1763
+ * @param {string[]} allowedRoots list of absolute paths the result must be inside
1764
+ * @returns {string} validated absolute real-path
1765
+ * @throws if requested is not contained within any allowed root
1766
+ */
1767
+ function safePath$3(requested, allowedRoots) {
1768
+ if (typeof requested !== "string" || !requested) {
1769
+ throw new Error("safePath: requested must be a non-empty string");
1770
+ }
1771
+ if (!Array.isArray(allowedRoots) || allowedRoots.length === 0) {
1772
+ throw new Error("safePath: allowedRoots must be a non-empty array");
1773
+ }
1774
+
1775
+ const resolved = path$i.resolve(requested);
1776
+
1777
+ // Real-path through symlinks. If the file doesn't exist yet (a
1778
+ // create-new operation), real-path the parent so a symlink in the
1779
+ // parent chain can't trick us.
1780
+ let real = resolved;
1781
+ try {
1782
+ real = fs$d.realpathSync(resolved);
1783
+ } catch (_e) {
1784
+ try {
1785
+ const parent = fs$d.realpathSync(path$i.dirname(resolved));
1786
+ real = path$i.join(parent, path$i.basename(resolved));
1787
+ } catch (_e2) {
1788
+ // Parent doesn't exist either. Use the resolved-but-not-
1789
+ // real path; the caller's mkdirSync will happen inside the
1790
+ // validated root, and any symlinks underneath will be
1791
+ // re-checked the next time safePath sees the same path.
1792
+ }
1793
+ }
1794
+
1795
+ for (const root of allowedRoots) {
1796
+ let realRoot = root;
1797
+ try {
1798
+ if (fs$d.existsSync(root)) realRoot = fs$d.realpathSync(root);
1799
+ } catch (_e) {
1800
+ // root doesn't exist or isn't reachable — keep as-is for
1801
+ // the comparison below
1802
+ }
1803
+ // Exact match OR strictly-inside (with separator to prevent
1804
+ // /data-evil/ matching /data/).
1805
+ if (real === realRoot || real.startsWith(realRoot + path$i.sep)) {
1806
+ return real;
1807
+ }
1808
+ }
1809
+
1810
+ throw new Error(
1811
+ "safePath: requested path is not within any allowed root. " +
1812
+ "Requested: " +
1813
+ requested +
1814
+ " (resolved to " +
1815
+ real +
1816
+ "). Allowed roots: " +
1817
+ allowedRoots.join(", "),
1818
+ );
1819
+ }
1820
+
1821
+ var safePath_1 = {
1822
+ safePath: safePath$3,
1823
+ getAllowedRoots: getAllowedRoots$2,
1824
+ };
1825
+
1653
1826
  /**
1654
1827
  * safeJsExecutor.js
1655
1828
  *
@@ -1887,15 +2060,15 @@ var safeJsExecutor$1 = {
1887
2060
  * - CSV -> JSON
1888
2061
  */
1889
2062
 
1890
- var fs$b = require$$0$2;
2063
+ var fs$c = require$$0$2;
1891
2064
  var readline = require$$1$3;
1892
2065
  const xtreamer = require$$2;
1893
2066
  var xmlParser = require$$3$1;
1894
2067
  var JSONStream$1 = require$$4;
1895
2068
  const stream$1 = require$$5;
1896
2069
  var csv = require$$6;
1897
- const path$g = require$$1$2;
1898
- const { app: app$9 } = require$$0$1;
2070
+ const path$h = require$$1$2;
2071
+ const { app: app$b } = require$$0$1;
1899
2072
  const { ensureDirectoryExistence: ensureDirectoryExistence$1 } = file;
1900
2073
  const safeJsExecutor = safeJsExecutor$1;
1901
2074
 
@@ -1949,7 +2122,7 @@ let Transform$1 = class Transform {
1949
2122
  let lineObject = [];
1950
2123
 
1951
2124
  const readInterface = readline.createInterface({
1952
- input: fs$b.createReadStream(filepath),
2125
+ input: fs$c.createReadStream(filepath),
1953
2126
  output: process.stdout,
1954
2127
  console: false,
1955
2128
  });
@@ -1984,7 +2157,7 @@ let Transform$1 = class Transform {
1984
2157
  return new Promise((resolve, reject) => {
1985
2158
  try {
1986
2159
  const parser = JSONStream$1.parse("*");
1987
- const readStream = fs$b.createReadStream(file).pipe(parser);
2160
+ const readStream = fs$c.createReadStream(file).pipe(parser);
1988
2161
 
1989
2162
  let count = 0;
1990
2163
 
@@ -2037,7 +2210,7 @@ let Transform$1 = class Transform {
2037
2210
  parseXMLStream = (filepath, outpath, start) => {
2038
2211
  return new Promise((resolve, reject) => {
2039
2212
  try {
2040
- const xmlFileReadStream = fs$b.createReadStream(filepath);
2213
+ const xmlFileReadStream = fs$c.createReadStream(filepath);
2041
2214
 
2042
2215
  xmlFileReadStream.on("end", () => {
2043
2216
  writeStream.write("\n]");
@@ -2048,7 +2221,7 @@ let Transform$1 = class Transform {
2048
2221
  resolve("Read Finish");
2049
2222
  });
2050
2223
 
2051
- const writeStream = fs$b.createWriteStream(outpath);
2224
+ const writeStream = fs$c.createWriteStream(outpath);
2052
2225
  writeStream.write("[\n");
2053
2226
 
2054
2227
  const options = {
@@ -2100,10 +2273,10 @@ let Transform$1 = class Transform {
2100
2273
  ) => {
2101
2274
  return new Promise((resolve, reject) => {
2102
2275
  try {
2103
- const readStream = fs$b
2276
+ const readStream = fs$c
2104
2277
  .createReadStream(filepath)
2105
2278
  .pipe(csv({ separator: delimiter }));
2106
- const writeStream = fs$b.createWriteStream(outpath);
2279
+ const writeStream = fs$c.createWriteStream(outpath);
2107
2280
 
2108
2281
  let canParse = true;
2109
2282
 
@@ -2189,18 +2362,18 @@ let Transform$1 = class Transform {
2189
2362
  }
2190
2363
 
2191
2364
  // Validate file paths are within app data directory
2192
- const appDataDir = path$g.join(app$9.getPath("userData"), TRANSFORM_APP_NAME);
2193
- const resolvedFilepath = path$g.resolve(filepath);
2194
- const resolvedOutFilepath = path$g.resolve(outFilepath);
2365
+ const appDataDir = path$h.join(app$b.getPath("userData"), TRANSFORM_APP_NAME);
2366
+ const resolvedFilepath = path$h.resolve(filepath);
2367
+ const resolvedOutFilepath = path$h.resolve(outFilepath);
2195
2368
 
2196
- if (!resolvedFilepath.startsWith(appDataDir + path$g.sep)) {
2369
+ if (!resolvedFilepath.startsWith(appDataDir + path$h.sep)) {
2197
2370
  return reject(
2198
2371
  new Error(
2199
2372
  "Input file path must be within the application data directory",
2200
2373
  ),
2201
2374
  );
2202
2375
  }
2203
- if (!resolvedOutFilepath.startsWith(appDataDir + path$g.sep)) {
2376
+ if (!resolvedOutFilepath.startsWith(appDataDir + path$h.sep)) {
2204
2377
  return reject(
2205
2378
  new Error(
2206
2379
  "Output file path must be within the application data directory",
@@ -2211,16 +2384,16 @@ let Transform$1 = class Transform {
2211
2384
  // JSON parser
2212
2385
  var parser = JSONStream$1.parse("*");
2213
2386
 
2214
- if (!fs$b.existsSync(resolvedFilepath)) {
2387
+ if (!fs$c.existsSync(resolvedFilepath)) {
2215
2388
  return reject(new Error("File doesnt exist"));
2216
2389
  }
2217
2390
  console.log("file exists ", resolvedFilepath);
2218
2391
  // create the readStream to parse the large file (json)
2219
- var readStream = fs$b.createReadStream(resolvedFilepath).pipe(parser);
2392
+ var readStream = fs$c.createReadStream(resolvedFilepath).pipe(parser);
2220
2393
 
2221
2394
  ensureDirectoryExistence$1(resolvedOutFilepath);
2222
2395
 
2223
- var writeStream = fs$b.createWriteStream(resolvedOutFilepath);
2396
+ var writeStream = fs$c.createWriteStream(resolvedOutFilepath);
2224
2397
 
2225
2398
  let sep = "";
2226
2399
  let count = 0;
@@ -2332,16 +2505,17 @@ let Transform$1 = class Transform {
2332
2505
 
2333
2506
  var transform = Transform$1;
2334
2507
 
2335
- const { app: app$8 } = require$$0$1;
2336
- var fs$a = require$$0$2;
2337
- const path$f = require$$1$2;
2508
+ const { app: app$a } = require$$0$1;
2509
+ var fs$b = require$$0$2;
2510
+ const path$g = require$$1$2;
2338
2511
  const events$5 = events$8;
2339
2512
  const { getFileContents: getFileContents$5, writeToFile: writeToFile$2 } = file;
2513
+ const { safePath: safePath$2, getAllowedRoots: getAllowedRoots$1 } = safePath_1;
2340
2514
 
2341
2515
  // Convert Json to Csv
2342
- const ObjectsToCsv = require$$5$1;
2516
+ const ObjectsToCsv = require$$6$1;
2343
2517
  const Transform = transform;
2344
- const https$3 = require$$7;
2518
+ const https$3 = require$$8$1;
2345
2519
  const appName$5 = "Dashboard";
2346
2520
 
2347
2521
  const dataController$1 = {
@@ -2355,14 +2529,25 @@ const dataController$1 = {
2355
2529
  */
2356
2530
  convertJsonToCsvFile: (win, appId, jsonObject, toFilename = "test.csv") => {
2357
2531
  try {
2358
- // filename to the pages file (live pages)
2359
- const filename = path$f.join(
2360
- app$8.getPath("userData"),
2532
+ // Validate the renderer-supplied filename is contained within
2533
+ // the data directory. path.join doesn't reject `..` segments;
2534
+ // safePath does.
2535
+ const candidate = path$g.join(
2536
+ app$a.getPath("userData"),
2361
2537
  appName$5,
2362
2538
  appId,
2363
2539
  "data",
2364
2540
  toFilename,
2365
2541
  );
2542
+ let filename;
2543
+ try {
2544
+ filename = safePath$2(candidate, getAllowedRoots$1("data"));
2545
+ } catch (pathErr) {
2546
+ win.webContents.send(events$5.DATA_JSON_TO_CSV_FILE_ERROR, {
2547
+ error: pathErr.message,
2548
+ });
2549
+ return;
2550
+ }
2366
2551
 
2367
2552
  // make sure the file exists...
2368
2553
  const fileContents = getFileContents$5(filename, "");
@@ -2420,8 +2605,17 @@ const dataController$1 = {
2420
2605
 
2421
2606
  readLinesFromFile: (win, filepath, lineCount) => {
2422
2607
  try {
2608
+ let validated;
2609
+ try {
2610
+ validated = safePath$2(filepath, getAllowedRoots$1("data"));
2611
+ } catch (pathErr) {
2612
+ win.webContents.send(events$5.READ_LINES_ERROR, {
2613
+ error: pathErr.message,
2614
+ });
2615
+ return;
2616
+ }
2423
2617
  const t = new Transform();
2424
- t.readLinesFromFile(win, filepath, lineCount, events$5.READ_LINES_UPDATE)
2618
+ t.readLinesFromFile(win, validated, lineCount, events$5.READ_LINES_UPDATE)
2425
2619
  .then((res) => {
2426
2620
  win.webContents.send(events$5.READ_LINES_COMPLETE, {
2427
2621
  success: true,
@@ -2445,9 +2639,18 @@ const dataController$1 = {
2445
2639
 
2446
2640
  readJSONFromFile: (win, filepath, objectCount = null) => {
2447
2641
  try {
2448
- console.log("reading json from file ", filepath, objectCount);
2642
+ let validated;
2643
+ try {
2644
+ validated = safePath$2(filepath, getAllowedRoots$1("data"));
2645
+ } catch (pathErr) {
2646
+ win.webContents.send(events$5.READ_JSON_ERROR, {
2647
+ error: pathErr.message,
2648
+ });
2649
+ return;
2650
+ }
2651
+ console.log("reading json from file ", validated, objectCount);
2449
2652
  const t = new Transform();
2450
- t.readJSONFromFile(win, filepath, objectCount, events$5.READ_JSON_UPDATE)
2653
+ t.readJSONFromFile(win, validated, objectCount, events$5.READ_JSON_UPDATE)
2451
2654
  .then((res) => {
2452
2655
  win.webContents.send(events$5.READ_JSON_COMPLETE, {
2453
2656
  success: true,
@@ -2483,16 +2686,12 @@ const dataController$1 = {
2483
2686
  );
2484
2687
  }
2485
2688
 
2486
- // Validate toFilepath is within the app data directory
2487
- const appDataDir = path$f.join(app$8.getPath("userData"), appName$5);
2488
- const resolvedFilepath = path$f.resolve(toFilepath);
2489
- if (!resolvedFilepath.startsWith(appDataDir + path$f.sep)) {
2490
- throw new Error(
2491
- "File path must be within the application data directory",
2492
- );
2493
- }
2689
+ // Validate toFilepath is within the app data directory.
2690
+ // safePath replaces the previous inline check; same containment
2691
+ // intent, plus realpath/symlink protection.
2692
+ const resolvedFilepath = safePath$2(toFilepath, getAllowedRoots$1("data"));
2494
2693
 
2495
- const writeStream = fs$a.createWriteStream(resolvedFilepath);
2694
+ const writeStream = fs$b.createWriteStream(resolvedFilepath);
2496
2695
 
2497
2696
  https$3
2498
2697
  .get(url, (resp) => {
@@ -2537,10 +2736,21 @@ const dataController$1 = {
2537
2736
  objectIdKey = null,
2538
2737
  ) => {
2539
2738
  try {
2739
+ let validatedIn, validatedOut;
2740
+ try {
2741
+ const roots = getAllowedRoots$1("data");
2742
+ validatedIn = safePath$2(filepath, roots);
2743
+ validatedOut = safePath$2(outpath, roots);
2744
+ } catch (pathErr) {
2745
+ win.webContents.send(events$5.PARSE_XML_STREAM_ERROR, {
2746
+ error: pathErr.message,
2747
+ });
2748
+ return;
2749
+ }
2540
2750
  const t = new Transform();
2541
2751
  t.parseXMLStream(
2542
- filepath,
2543
- outpath,
2752
+ validatedIn,
2753
+ validatedOut,
2544
2754
  start,
2545
2755
  // recordNode,
2546
2756
  // objectIdKey,
@@ -2586,10 +2796,21 @@ const dataController$1 = {
2586
2796
  limit = null,
2587
2797
  ) => {
2588
2798
  try {
2799
+ let validatedIn, validatedOut;
2800
+ try {
2801
+ const roots = getAllowedRoots$1("data");
2802
+ validatedIn = safePath$2(filepath, roots);
2803
+ validatedOut = safePath$2(outpath, roots);
2804
+ } catch (pathErr) {
2805
+ win.webContents.send(events$5.PARSE_CSV_STREAM_ERROR, {
2806
+ error: pathErr.message,
2807
+ });
2808
+ return;
2809
+ }
2589
2810
  const t = new Transform();
2590
2811
  t.parseCSVStream(
2591
- filepath,
2592
- outpath,
2812
+ validatedIn,
2813
+ validatedOut,
2593
2814
  delimiter,
2594
2815
  objectIdKey,
2595
2816
  headers,
@@ -2632,13 +2853,25 @@ const dataController$1 = {
2632
2853
  saveToFile: (win, data, filename, append, returnEmpty = {}) => {
2633
2854
  try {
2634
2855
  if (data) {
2635
- // filename to the pages file (live pages)
2636
- const toFilename = path$f.join(
2637
- app$8.getPath("userData"),
2856
+ // Validate filename is contained within the data directory.
2857
+ // path.join doesn't reject `..` segments; safePath does.
2858
+ const candidate = path$g.join(
2859
+ app$a.getPath("userData"),
2638
2860
  appName$5,
2639
2861
  "data",
2640
2862
  filename,
2641
2863
  );
2864
+ let toFilename;
2865
+ try {
2866
+ toFilename = safePath$2(candidate, getAllowedRoots$1("data"));
2867
+ } catch (pathErr) {
2868
+ win.webContents.send(events$5.DATA_SAVE_TO_FILE_ERROR, {
2869
+ success: false,
2870
+ filename,
2871
+ message: pathErr.message,
2872
+ });
2873
+ return;
2874
+ }
2642
2875
 
2643
2876
  //console.log("saving to file ", toFilename);
2644
2877
 
@@ -2715,8 +2948,8 @@ const dataController$1 = {
2715
2948
  try {
2716
2949
  if (filename) {
2717
2950
  // filename to the pages file (live pages)
2718
- const fromFilename = path$f.join(
2719
- app$8.getPath("userData"),
2951
+ const fromFilename = path$g.join(
2952
+ app$a.getPath("userData"),
2720
2953
  appName$5,
2721
2954
  "data",
2722
2955
  filename,
@@ -2796,9 +3029,9 @@ var dataController_1 = dataController$1;
2796
3029
  * settingsController
2797
3030
  */
2798
3031
 
2799
- const { app: app$7 } = require$$0$1;
2800
- const path$e = require$$1$2;
2801
- const fs$9 = require$$0$2;
3032
+ const { app: app$9 } = require$$0$1;
3033
+ const path$f = require$$1$2;
3034
+ const fs$a = require$$0$2;
2802
3035
  const { getFileContents: getFileContents$4, writeToFile: writeToFile$1 } = file;
2803
3036
 
2804
3037
  const configFilename$3 = "settings.json";
@@ -2806,15 +3039,15 @@ const appName$4 = "Dashboard";
2806
3039
 
2807
3040
  // Helper function to recursively copy directory
2808
3041
  function copyDirectory(source, destination) {
2809
- if (!fs$9.existsSync(destination)) {
2810
- fs$9.mkdirSync(destination, { recursive: true });
3042
+ if (!fs$a.existsSync(destination)) {
3043
+ fs$a.mkdirSync(destination, { recursive: true });
2811
3044
  }
2812
3045
 
2813
- const files = fs$9.readdirSync(source);
3046
+ const files = fs$a.readdirSync(source);
2814
3047
  for (const file of files) {
2815
- const srcPath = path$e.join(source, file);
2816
- const destPath = path$e.join(destination, file);
2817
- const stat = fs$9.lstatSync(srcPath);
3048
+ const srcPath = path$f.join(source, file);
3049
+ const destPath = path$f.join(destination, file);
3050
+ const stat = fs$a.lstatSync(srcPath);
2818
3051
 
2819
3052
  // Skip symlinks to prevent following links to sensitive files
2820
3053
  if (stat.isSymbolicLink()) {
@@ -2825,7 +3058,7 @@ function copyDirectory(source, destination) {
2825
3058
  if (stat.isDirectory()) {
2826
3059
  copyDirectory(srcPath, destPath);
2827
3060
  } else {
2828
- fs$9.copyFileSync(srcPath, destPath);
3061
+ fs$a.copyFileSync(srcPath, destPath);
2829
3062
  }
2830
3063
  }
2831
3064
  }
@@ -2841,8 +3074,8 @@ const settingsController$4 = {
2841
3074
  try {
2842
3075
  if (data) {
2843
3076
  // <appId>/settings.json
2844
- const filename = path$e.join(
2845
- app$7.getPath("userData"),
3077
+ const filename = path$f.join(
3078
+ app$9.getPath("userData"),
2846
3079
  appName$4,
2847
3080
  configFilename$3,
2848
3081
  );
@@ -2877,8 +3110,8 @@ const settingsController$4 = {
2877
3110
  getSettingsForApplication: (win) => {
2878
3111
  try {
2879
3112
  // <appId>/settings.json
2880
- const filename = path$e.join(
2881
- app$7.getPath("userData"),
3113
+ const filename = path$f.join(
3114
+ app$9.getPath("userData"),
2882
3115
  appName$4,
2883
3116
  configFilename$3,
2884
3117
  );
@@ -2908,15 +3141,15 @@ const settingsController$4 = {
2908
3141
  */
2909
3142
  getDataDirectory: (win) => {
2910
3143
  try {
2911
- const settingsPath = path$e.join(
2912
- app$7.getPath("userData"),
3144
+ const settingsPath = path$f.join(
3145
+ app$9.getPath("userData"),
2913
3146
  appName$4,
2914
3147
  configFilename$3,
2915
3148
  );
2916
3149
  const settings = getFileContents$4(settingsPath, {});
2917
3150
  const userDataDir =
2918
3151
  settings.userDataDirectory ||
2919
- path$e.join(app$7.getPath("userData"), appName$4);
3152
+ path$f.join(app$9.getPath("userData"), appName$4);
2920
3153
 
2921
3154
  console.log("[settingsController] Data directory retrieved successfully");
2922
3155
  // Return the data for ipcMain.handle() - modern promise-based approach
@@ -2943,18 +3176,18 @@ const settingsController$4 = {
2943
3176
  setDataDirectory: (win, newPath) => {
2944
3177
  try {
2945
3178
  // Validate the path exists and is a directory
2946
- if (!fs$9.existsSync(newPath)) {
2947
- fs$9.mkdirSync(newPath, { recursive: true });
3179
+ if (!fs$a.existsSync(newPath)) {
3180
+ fs$a.mkdirSync(newPath, { recursive: true });
2948
3181
  }
2949
3182
 
2950
- const stats = fs$9.statSync(newPath);
3183
+ const stats = fs$a.statSync(newPath);
2951
3184
  if (!stats.isDirectory()) {
2952
3185
  throw new Error("Path is not a directory");
2953
3186
  }
2954
3187
 
2955
3188
  // Update settings
2956
- const settingsPath = path$e.join(
2957
- app$7.getPath("userData"),
3189
+ const settingsPath = path$f.join(
3190
+ app$9.getPath("userData"),
2958
3191
  appName$4,
2959
3192
  configFilename$3,
2960
3193
  );
@@ -2987,20 +3220,20 @@ const settingsController$4 = {
2987
3220
  migrateDataDirectory: (win, oldPath, newPath) => {
2988
3221
  try {
2989
3222
  // Resolve paths to prevent traversal
2990
- const resolvedOldPath = path$e.resolve(oldPath);
2991
- const resolvedNewPath = path$e.resolve(newPath);
3223
+ const resolvedOldPath = path$f.resolve(oldPath);
3224
+ const resolvedNewPath = path$f.resolve(newPath);
2992
3225
 
2993
3226
  // Validate oldPath is the current configured data directory
2994
- const settingsCheckPath = path$e.join(
2995
- app$7.getPath("userData"),
3227
+ const settingsCheckPath = path$f.join(
3228
+ app$9.getPath("userData"),
2996
3229
  appName$4,
2997
3230
  configFilename$3,
2998
3231
  );
2999
3232
  const currentSettings = getFileContents$4(settingsCheckPath, {});
3000
3233
  const currentDataDir =
3001
3234
  currentSettings.userDataDirectory ||
3002
- path$e.join(app$7.getPath("userData"), appName$4);
3003
- if (resolvedOldPath !== path$e.resolve(currentDataDir)) {
3235
+ path$f.join(app$9.getPath("userData"), appName$4);
3236
+ if (resolvedOldPath !== path$f.resolve(currentDataDir)) {
3004
3237
  throw new Error("Source path must be the current data directory");
3005
3238
  }
3006
3239
 
@@ -3024,20 +3257,20 @@ const settingsController$4 = {
3024
3257
  }
3025
3258
 
3026
3259
  // Validate paths
3027
- if (!fs$9.existsSync(resolvedOldPath)) {
3260
+ if (!fs$a.existsSync(resolvedOldPath)) {
3028
3261
  throw new Error("Source directory does not exist");
3029
3262
  }
3030
3263
 
3031
- if (!fs$9.existsSync(resolvedNewPath)) {
3032
- fs$9.mkdirSync(resolvedNewPath, { recursive: true });
3264
+ if (!fs$a.existsSync(resolvedNewPath)) {
3265
+ fs$a.mkdirSync(resolvedNewPath, { recursive: true });
3033
3266
  }
3034
3267
 
3035
3268
  // Copy files
3036
3269
  copyDirectory(resolvedOldPath, resolvedNewPath);
3037
3270
 
3038
3271
  // Update settings to use new path
3039
- const settingsPath = path$e.join(
3040
- app$7.getPath("userData"),
3272
+ const settingsPath = path$f.join(
3273
+ app$9.getPath("userData"),
3041
3274
  appName$4,
3042
3275
  configFilename$3,
3043
3276
  );
@@ -3701,8 +3934,8 @@ function requireProviderController () {
3701
3934
  return providerController_1;
3702
3935
  }
3703
3936
 
3704
- const { app: app$6 } = require$$0$1;
3705
- const path$d = require$$1$2;
3937
+ const { app: app$8 } = require$$0$1;
3938
+ const path$e = require$$1$2;
3706
3939
  const { writeFileSync: writeFileSync$1 } = require$$0$2;
3707
3940
  const events$4 = events$8;
3708
3941
  const { getFileContents: getFileContents$3 } = file;
@@ -3722,8 +3955,8 @@ const layoutController$1 = {
3722
3955
  saveLayoutForApplication: (win, appId, layoutObject) => {
3723
3956
  try {
3724
3957
  // filename to the pages file (live pages)
3725
- const filename = path$d.join(
3726
- app$6.getPath("userData"),
3958
+ const filename = path$e.join(
3959
+ app$8.getPath("userData"),
3727
3960
  appName$3,
3728
3961
  appId,
3729
3962
  configFilename$2,
@@ -3755,8 +3988,8 @@ const layoutController$1 = {
3755
3988
  */
3756
3989
  listLayoutsForApplication: (win, appId) => {
3757
3990
  try {
3758
- const filename = path$d.join(
3759
- app$6.getPath("userData"),
3991
+ const filename = path$e.join(
3992
+ app$8.getPath("userData"),
3760
3993
  appName$3,
3761
3994
  appId,
3762
3995
  configFilename$2,
@@ -20919,6 +21152,311 @@ let StreamableHTTPClientTransport$1 = class StreamableHTTPClientTransport {
20919
21152
  };
20920
21153
  streamableHttp$1.StreamableHTTPClientTransport = StreamableHTTPClientTransport$1;
20921
21154
 
21155
+ /**
21156
+ * widgetPermissions.js
21157
+ *
21158
+ * Read and parse the `dash.permissions.mcp` block from an installed
21159
+ * widget's package.json.
21160
+ *
21161
+ * Manifest format (declared by widget authors in their package.json):
21162
+ *
21163
+ * {
21164
+ * "name": "@trops/notes-summarizer",
21165
+ * "dash": {
21166
+ * "permissions": {
21167
+ * "mcp": {
21168
+ * "filesystem": {
21169
+ * "tools": ["read_file", "list_directory"],
21170
+ * "readPaths": ["~/Documents/notes"],
21171
+ * "writePaths": []
21172
+ * },
21173
+ * "github": {
21174
+ * "tools": ["search_repositories", "get_file_contents"]
21175
+ * }
21176
+ * }
21177
+ * }
21178
+ * }
21179
+ * }
21180
+ *
21181
+ * Path strings beginning with `~` are expanded to the user's home
21182
+ * directory at parse time. Tool-only servers (no path I/O, e.g.
21183
+ * github) omit the `readPaths`/`writePaths` keys.
21184
+ *
21185
+ * Public API:
21186
+ *
21187
+ * getWidgetMcpPermissions(widgetId) → permissions | null
21188
+ * Returns the parsed permissions for a widget, or null if the
21189
+ * widget is unmanifested. Cached per process.
21190
+ *
21191
+ * parseManifestPermissions(packageJson) → permissions | null
21192
+ * Pure function — exposed for tests.
21193
+ *
21194
+ * clearCache() → void
21195
+ * Test-only. Drops the in-process cache so tests can re-read.
21196
+ */
21197
+
21198
+ const fs$9 = require$$0$2;
21199
+ const path$d = require$$1$2;
21200
+ const os$2 = require$$2$1;
21201
+ const { app: app$7 } = require$$0$1;
21202
+
21203
+ // Cache: widgetId → permissions | null. Populated lazily on first
21204
+ // lookup; invalidated when a widget is installed/uninstalled (the
21205
+ // install/uninstall paths call clearCache()).
21206
+ const _cache = new Map();
21207
+
21208
+ /**
21209
+ * Expand a leading "~" to the user's home directory. Other paths are
21210
+ * returned as-is.
21211
+ */
21212
+ function expandHome(p) {
21213
+ if (typeof p !== "string" || !p) return p;
21214
+ if (p === "~") return os$2.homedir();
21215
+ if (p.startsWith("~/") || p.startsWith("~\\")) {
21216
+ return path$d.join(os$2.homedir(), p.slice(2));
21217
+ }
21218
+ return p;
21219
+ }
21220
+
21221
+ /**
21222
+ * Parse a widget's package.json contents into a normalized permissions
21223
+ * object. Returns null if no `dash.permissions.mcp` block exists.
21224
+ */
21225
+ function parseManifestPermissions(packageJson) {
21226
+ if (!packageJson || typeof packageJson !== "object") return null;
21227
+ const mcp = packageJson?.dash?.permissions?.mcp;
21228
+ if (!mcp || typeof mcp !== "object") return null;
21229
+
21230
+ const servers = {};
21231
+ for (const [serverName, raw] of Object.entries(mcp)) {
21232
+ if (!raw || typeof raw !== "object") continue;
21233
+ const tools = Array.isArray(raw.tools)
21234
+ ? raw.tools.filter((t) => typeof t === "string")
21235
+ : [];
21236
+ const readPaths = Array.isArray(raw.readPaths)
21237
+ ? raw.readPaths.filter((p) => typeof p === "string").map(expandHome)
21238
+ : [];
21239
+ const writePaths = Array.isArray(raw.writePaths)
21240
+ ? raw.writePaths.filter((p) => typeof p === "string").map(expandHome)
21241
+ : [];
21242
+ servers[serverName] = { tools, readPaths, writePaths };
21243
+ }
21244
+
21245
+ return { servers };
21246
+ }
21247
+
21248
+ /**
21249
+ * Find a widget's installed package.json on disk. Widgets live under
21250
+ * userData/widgets/<scope>/<name>/ for scoped packages or
21251
+ * userData/widgets/<name>/ for unscoped. The widgetId is the npm
21252
+ * package name (e.g. "@trops/notes-summarizer" or "notes-summarizer").
21253
+ */
21254
+ function resolveWidgetPackagePath(widgetId) {
21255
+ if (typeof widgetId !== "string" || !widgetId) return null;
21256
+ const widgetsRoot = path$d.join(app$7.getPath("userData"), "widgets");
21257
+ // Split scope from name for "@scope/name" form.
21258
+ const parts = widgetId.startsWith("@") ? widgetId.split("/") : [widgetId];
21259
+ return path$d.join(widgetsRoot, ...parts, "package.json");
21260
+ }
21261
+
21262
+ /**
21263
+ * Read and parse a widget's MCP permissions. Returns null if:
21264
+ * - the widget directory doesn't exist
21265
+ * - package.json is unreadable / malformed
21266
+ * - the widget hasn't declared dash.permissions.mcp
21267
+ *
21268
+ * Result is cached per widgetId for the lifetime of this process.
21269
+ */
21270
+ function getWidgetMcpPermissions$1(widgetId) {
21271
+ if (_cache.has(widgetId)) return _cache.get(widgetId);
21272
+ const pkgPath = resolveWidgetPackagePath(widgetId);
21273
+ if (!pkgPath || !fs$9.existsSync(pkgPath)) {
21274
+ _cache.set(widgetId, null);
21275
+ return null;
21276
+ }
21277
+ try {
21278
+ const raw = fs$9.readFileSync(pkgPath, "utf8");
21279
+ const pkg = JSON.parse(raw);
21280
+ const perms = parseManifestPermissions(pkg);
21281
+ _cache.set(widgetId, perms);
21282
+ return perms;
21283
+ } catch (e) {
21284
+ console.warn(
21285
+ "[widgetPermissions] failed to read package.json for " +
21286
+ widgetId +
21287
+ ": " +
21288
+ e.message,
21289
+ );
21290
+ _cache.set(widgetId, null);
21291
+ return null;
21292
+ }
21293
+ }
21294
+
21295
+ function clearCache() {
21296
+ _cache.clear();
21297
+ }
21298
+
21299
+ var widgetPermissions = {
21300
+ getWidgetMcpPermissions: getWidgetMcpPermissions$1,
21301
+ parseManifestPermissions,
21302
+ expandHome,
21303
+ clearCache,
21304
+ };
21305
+
21306
+ /**
21307
+ * permissionGate.js
21308
+ *
21309
+ * Per-widget gating for MCP tool calls.
21310
+ *
21311
+ * When `gateToolCall` is invoked with a widget identity, server name,
21312
+ * tool name, and tool arguments, it consults the widget's installed
21313
+ * permission manifest (electron/mcp/widgetPermissions.js) and either
21314
+ * permits the call or returns a clear denial reason.
21315
+ *
21316
+ * Two layers:
21317
+ *
21318
+ * 1. **Tool-name allowlist** — the manifest's `tools[]` array for the
21319
+ * target server determines which tool names this widget may
21320
+ * invoke. Anything outside the list is rejected.
21321
+ *
21322
+ * 2. **Path-argument containment** — for tools whose arguments
21323
+ * include a path-shaped key (`path`, `uri`, `filepath`, `file`,
21324
+ * `directory`), the supplied path is validated with safePath()
21325
+ * against the widget's declared `readPaths` or `writePaths` for
21326
+ * the target server. The read/write distinction is heuristic
21327
+ * based on the tool name (e.g. `write_file` is treated as a
21328
+ * write).
21329
+ *
21330
+ * This is the runtime enforcement layer. Install-time consent UI and
21331
+ * per-dashboard MCP-server scope reconfiguration are separate plans
21332
+ * (Slices 2 and 3). When the feature flag is OFF (default), this gate
21333
+ * is bypassed entirely; mcpController behaves as before. When ON,
21334
+ * every callTool dispatch goes through this gate.
21335
+ */
21336
+
21337
+ const { getWidgetMcpPermissions } = widgetPermissions;
21338
+ const { safePath: safePath$1 } = safePath_1;
21339
+
21340
+ // Argument keys that look like paths. Different MCP servers use
21341
+ // different conventions; this list covers the common filesystem-style
21342
+ // servers. Extensible — add as new patterns surface.
21343
+ const PATH_ARG_KEYS = ["path", "uri", "filepath", "file", "directory"];
21344
+
21345
+ // Heuristic: tool names matching this regex are treated as writes for
21346
+ // purposes of choosing readPaths vs writePaths. The match is intentionally
21347
+ // broad — we'd rather treat an ambiguous tool as a write (stricter) than
21348
+ // as a read.
21349
+ const WRITE_TOOL_PATTERN =
21350
+ /(^|_)(write|create|edit|delete|remove|append|move|rename|chmod|chown|mkdir)/i;
21351
+
21352
+ function isWriteTool(toolName) {
21353
+ if (typeof toolName !== "string") return false;
21354
+ return WRITE_TOOL_PATTERN.test(toolName);
21355
+ }
21356
+
21357
+ /**
21358
+ * @returns {{ allow: true } | { allow: false, reason: string }}
21359
+ */
21360
+ function gateToolCall$1({ widgetId, serverName, toolName, args }) {
21361
+ if (!widgetId) {
21362
+ return {
21363
+ allow: false,
21364
+ reason: "no widgetId supplied; cannot determine permissions",
21365
+ };
21366
+ }
21367
+
21368
+ const perms = getWidgetMcpPermissions(widgetId);
21369
+ if (!perms) {
21370
+ return {
21371
+ allow: false,
21372
+ reason:
21373
+ "widget '" +
21374
+ widgetId +
21375
+ "' has no MCP permission manifest; declare dash.permissions.mcp in its package.json to grant access",
21376
+ };
21377
+ }
21378
+
21379
+ const serverPerms = perms.servers[serverName];
21380
+ if (!serverPerms) {
21381
+ return {
21382
+ allow: false,
21383
+ reason:
21384
+ "widget '" +
21385
+ widgetId +
21386
+ "' is not authorized to call '" +
21387
+ serverName +
21388
+ "'",
21389
+ };
21390
+ }
21391
+
21392
+ if (!serverPerms.tools.includes(toolName)) {
21393
+ return {
21394
+ allow: false,
21395
+ reason:
21396
+ "tool '" +
21397
+ toolName +
21398
+ "' is not in the allowlist for widget '" +
21399
+ widgetId +
21400
+ "' on server '" +
21401
+ serverName +
21402
+ "'",
21403
+ };
21404
+ }
21405
+
21406
+ // Path-argument containment. Only checked when the tool's args
21407
+ // include a path-shaped key.
21408
+ const isWrite = isWriteTool(toolName);
21409
+ // Write tools must use writePaths; read tools may use either
21410
+ // readPaths or writePaths (write access implies read access).
21411
+ const allowedPaths = isWrite
21412
+ ? serverPerms.writePaths
21413
+ : [...serverPerms.readPaths, ...serverPerms.writePaths];
21414
+
21415
+ if (args && typeof args === "object") {
21416
+ for (const key of PATH_ARG_KEYS) {
21417
+ const v = args[key];
21418
+ if (typeof v !== "string" || !v) continue;
21419
+ if (allowedPaths.length === 0) {
21420
+ return {
21421
+ allow: false,
21422
+ reason:
21423
+ "tool '" +
21424
+ toolName +
21425
+ "' uses path argument '" +
21426
+ key +
21427
+ "' but widget '" +
21428
+ widgetId +
21429
+ "' has no " +
21430
+ (isWrite ? "writePaths" : "readPaths or writePaths") +
21431
+ " declared for server '" +
21432
+ serverName +
21433
+ "'",
21434
+ };
21435
+ }
21436
+ try {
21437
+ safePath$1(v, allowedPaths);
21438
+ } catch (e) {
21439
+ return {
21440
+ allow: false,
21441
+ reason:
21442
+ "path argument '" +
21443
+ key +
21444
+ "' rejected: " +
21445
+ (e && e.message ? e.message : String(e)),
21446
+ };
21447
+ }
21448
+ }
21449
+ }
21450
+
21451
+ return { allow: true };
21452
+ }
21453
+
21454
+ var permissionGate = {
21455
+ gateToolCall: gateToolCall$1,
21456
+ isWriteTool,
21457
+ PATH_ARG_KEYS,
21458
+ };
21459
+
20922
21460
  /**
20923
21461
  * mcpController.js
20924
21462
  *
@@ -20943,6 +21481,28 @@ const path$c = require$$1$2;
20943
21481
  const fs$8 = require$$0$2;
20944
21482
  const os$1 = require$$2$1;
20945
21483
  const responseCache$2 = responseCache_1;
21484
+ const { gateToolCall } = permissionGate;
21485
+ const { app: app$6 } = require$$0$1;
21486
+
21487
+ // Read the widget-MCP-enforcement feature flag from settings.json.
21488
+ // Default is OFF — flipping ON activates per-widget gating in
21489
+ // permissionGate.gateToolCall(). See docs/security/ipc-filesystem-audit.md
21490
+ // and electron/mcp/permissionGate.js for context.
21491
+ function isWidgetPermissionEnforcementEnabled() {
21492
+ try {
21493
+ const settingsPath = path$c.join(
21494
+ app$6.getPath("userData"),
21495
+ "Dashboard",
21496
+ "settings.json",
21497
+ );
21498
+ if (!fs$8.existsSync(settingsPath)) return false;
21499
+ const raw = fs$8.readFileSync(settingsPath, "utf8");
21500
+ const settings = JSON.parse(raw);
21501
+ return Boolean(settings?.security?.enforceWidgetMcpPermissions);
21502
+ } catch (_e) {
21503
+ return false;
21504
+ }
21505
+ }
20946
21506
 
20947
21507
  /**
20948
21508
  * Tool name prefixes considered safe to cache (read-only).
@@ -21078,7 +21638,7 @@ function getShellPath$1() {
21078
21638
  return _shellPath$1;
21079
21639
  }
21080
21640
 
21081
- const { execSync } = require$$7$1;
21641
+ const { execSync } = require$$9$1;
21082
21642
  const fallbackDirs = ["/usr/local/bin", "/opt/homebrew/bin"];
21083
21643
 
21084
21644
  // Scan nvm versions, tracking both latest and best compatible version
@@ -21258,7 +21818,7 @@ async function refreshGoogleOAuthToken(tokenRefresh) {
21258
21818
 
21259
21819
  console.log("[mcpController] Refreshing Google OAuth token...");
21260
21820
 
21261
- const https = require$$7;
21821
+ const https = require$$8$1;
21262
21822
  const postData = [
21263
21823
  `client_id=${encodeURIComponent(keyData.client_id)}`,
21264
21824
  `client_secret=${encodeURIComponent(keyData.client_secret)}`,
@@ -21647,14 +22207,41 @@ const mcpController$3 = {
21647
22207
  * @param {Array<string>} allowedTools optional whitelist of allowed tool names
21648
22208
  * @returns {{ result } | { error, message }}
21649
22209
  */
21650
- callTool: async (win, serverName, toolName, args, allowedTools = null) => {
22210
+ callTool: async (
22211
+ win,
22212
+ serverName,
22213
+ toolName,
22214
+ args,
22215
+ allowedTools = null,
22216
+ widgetId = null,
22217
+ ) => {
21651
22218
  try {
21652
22219
  const server = activeServers.get(serverName);
21653
22220
  if (!server || !server.client) {
21654
22221
  throw new Error(`Server not connected: ${serverName}`);
21655
22222
  }
21656
22223
 
21657
- // Enforce tool scoping if allowedTools is specified
22224
+ // Per-widget manifest gate. Activated by the
22225
+ // security.enforceWidgetMcpPermissions setting. When enabled
22226
+ // and a widgetId is supplied, the widget's installed
22227
+ // package.json's dash.permissions.mcp block determines what
22228
+ // tools and paths are allowed.
22229
+ if (isWidgetPermissionEnforcementEnabled() && widgetId) {
22230
+ const gate = gateToolCall({
22231
+ widgetId,
22232
+ serverName,
22233
+ toolName,
22234
+ args,
22235
+ });
22236
+ if (!gate.allow) {
22237
+ throw new Error(`Widget permission gate: ${gate.reason}`);
22238
+ }
22239
+ }
22240
+
22241
+ // Legacy renderer-supplied allowedTools whitelist. Kept for
22242
+ // backward compatibility with callers that pre-date the
22243
+ // manifest-based gate. Once the manifest gate is enforced
22244
+ // everywhere, this can be removed.
21658
22245
  if (allowedTools && !allowedTools.includes(toolName)) {
21659
22246
  throw new Error(
21660
22247
  `Tool "${toolName}" is not in the allowed tools list for this widget. Allowed: ${allowedTools.join(
@@ -21951,7 +22538,7 @@ const mcpController$3 = {
21951
22538
  * @returns {{ success } | { error, message }}
21952
22539
  */
21953
22540
  runAuth: async (win, mcpConfig, credentials, authCommand) => {
21954
- const { spawn } = require$$7$1;
22541
+ const { spawn } = require$$9$1;
21955
22542
 
21956
22543
  const env = cleanEnvForChildProcess();
21957
22544
 
@@ -25973,6 +26560,7 @@ const algoliasearch = require$$2$3;
25973
26560
  const events$3 = events$8;
25974
26561
  const AlgoliaIndex = algolia;
25975
26562
  var fs$3 = require$$0$2;
26563
+ const { safePath, getAllowedRoots } = safePath_1;
25976
26564
 
25977
26565
  const algoliaController$1 = {
25978
26566
  /**
@@ -26129,10 +26717,19 @@ const algoliaController$1 = {
26129
26717
  createIfNotExists = false,
26130
26718
  ) {
26131
26719
  try {
26720
+ let validatedDir;
26721
+ try {
26722
+ validatedDir = safePath(dir, getAllowedRoots("data"));
26723
+ } catch (pathErr) {
26724
+ win.webContents.send(events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_ERROR, {
26725
+ error: pathErr.message,
26726
+ });
26727
+ return;
26728
+ }
26132
26729
  const a = new AlgoliaIndex(appId, apiKey, indexName);
26133
26730
  // now we can make the call to the utility and we are passing in the createIfNotExists FALSE by default
26134
26731
  a.partialUpdateObjectsFromDirectorySync(
26135
- dir,
26732
+ validatedDir,
26136
26733
  createIfNotExists,
26137
26734
  (data) => {
26138
26735
  win.webContents.send(
@@ -26172,10 +26769,21 @@ const algoliaController$1 = {
26172
26769
  batchSize = 500,
26173
26770
  ) => {
26174
26771
  try {
26772
+ let validatedIn, validatedOut;
26773
+ try {
26774
+ const roots = getAllowedRoots("data");
26775
+ validatedIn = safePath(filepath, roots);
26776
+ validatedOut = safePath(batchFilepath, roots);
26777
+ } catch (pathErr) {
26778
+ win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_ERROR, {
26779
+ error: pathErr.message,
26780
+ });
26781
+ return;
26782
+ }
26175
26783
  const a = new AlgoliaIndex();
26176
26784
  a.createBatchesFromJSONFile(
26177
- filepath,
26178
- batchFilepath,
26785
+ validatedIn,
26786
+ validatedOut,
26179
26787
  batchSize,
26180
26788
  (data) => {
26181
26789
  win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_UPDATE, data);
@@ -43641,7 +44249,7 @@ const completable_js_1 = completable;
43641
44249
  const uriTemplate_js_1 = uriTemplate;
43642
44250
  const toolNameValidation_js_1 = toolNameValidation;
43643
44251
  const mcp_server_js_1 = mcpServer$1;
43644
- const zod_1 = require$$8$1;
44252
+ const zod_1 = require$$8$2;
43645
44253
  /**
43646
44254
  * High-level MCP server that provides a simpler API for working with resources, tools, and prompts.
43647
44255
  * For advanced usage (like sending notifications or setting custom request handlers), use the underlying
@@ -46274,7 +46882,7 @@ var tlsCert = { getOrCreateCert: getOrCreateCert$1 };
46274
46882
  * for Zod schemas in tool input validation (safeParseAsync).
46275
46883
  */
46276
46884
 
46277
- const z$1 = require$$8$1;
46885
+ const z$1 = require$$8$2;
46278
46886
 
46279
46887
  /**
46280
46888
  * Convert a JSON Schema property definition to a Zod v3 schema.
@@ -46367,7 +46975,7 @@ var jsonSchemaToZod_1 = { jsonSchemaToZod: jsonSchemaToZod$1, jsonSchemaProperty
46367
46975
  * - Rate limiting via token bucket (60 req/min)
46368
46976
  */
46369
46977
 
46370
- const https$1 = require$$7;
46978
+ const https$1 = require$$8$1;
46371
46979
  const { randomUUID } = require$$3$4;
46372
46980
  const { BrowserWindow: BrowserWindow$1 } = require$$0$1;
46373
46981
  const { McpServer } = mcp;
@@ -46498,7 +47106,7 @@ function registerPrompt$1(promptDef) {
46498
47106
  registeredPrompts.push(promptDef);
46499
47107
  }
46500
47108
 
46501
- const z = require$$8$1;
47109
+ const z = require$$8$2;
46502
47110
  const { jsonSchemaToZod } = jsonSchemaToZod_1;
46503
47111
 
46504
47112
  /**
@@ -46879,7 +47487,7 @@ var mcpDashServerController_1 = mcpDashServerController$4;
46879
47487
  * can use the Chat widget without a separate API key.
46880
47488
  */
46881
47489
 
46882
- const { spawn, execSync } = require$$7$1;
47490
+ const { spawn, execSync } = require$$9$1;
46883
47491
  const {
46884
47492
  LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
46885
47493
  LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
@@ -48176,7 +48784,7 @@ var themeFromUrlErrors$1 = {
48176
48784
 
48177
48785
  const css = require$$0$8;
48178
48786
  const { Vibrant } = require$$1$7;
48179
- const https = require$$7;
48787
+ const https = require$$8$1;
48180
48788
  const http = require$$0$7;
48181
48789
  const { URL: URL$1 } = require$$4$1;
48182
48790
  const {
@@ -61216,15 +61824,27 @@ const mcpApi$2 = {
61216
61824
  * @param {string} serverName the server name
61217
61825
  * @param {string} toolName the tool to call
61218
61826
  * @param {object} args tool arguments
61219
- * @param {Array<string>} allowedTools optional whitelist of allowed tool names
61827
+ * @param {Array<string>} allowedTools optional whitelist of allowed tool names (legacy — prefer per-widget manifest)
61828
+ * @param {string} widgetId optional widget identity. When the
61829
+ * security.enforceWidgetMcpPermissions setting is enabled, this is
61830
+ * used to look up the widget's MCP permission manifest and gate
61831
+ * the call accordingly. Should be the npm package name of the
61832
+ * calling widget (e.g. "@trops/notes-summarizer").
61220
61833
  * @returns {Promise<{ result } | { error, message }>}
61221
61834
  */
61222
- callTool: (serverName, toolName, args, allowedTools = null) =>
61835
+ callTool: (
61836
+ serverName,
61837
+ toolName,
61838
+ args,
61839
+ allowedTools = null,
61840
+ widgetId = null,
61841
+ ) =>
61223
61842
  ipcRenderer$i.invoke(MCP_CALL_TOOL, {
61224
61843
  serverName,
61225
61844
  toolName,
61226
61845
  args,
61227
61846
  allowedTools,
61847
+ widgetId,
61228
61848
  }),
61229
61849
 
61230
61850
  /**