@trops/dash-core 0.1.500 → 0.1.502
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/electron/index.js +1570 -1163
- package/dist/electron/index.js.map +1 -1
- package/dist/index.esm.js +145 -36
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +145 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/electron/index.js
CHANGED
|
@@ -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$$
|
|
7
|
+
var require$$8$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,7 +12,7 @@ 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$$
|
|
15
|
+
var require$$10 = 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');
|
|
@@ -1107,7 +1107,7 @@ var secureStoreController$1 = {
|
|
|
1107
1107
|
getData: getData$1,
|
|
1108
1108
|
};
|
|
1109
1109
|
|
|
1110
|
-
const path$
|
|
1110
|
+
const path$m = require$$1$2;
|
|
1111
1111
|
const {
|
|
1112
1112
|
readFileSync,
|
|
1113
1113
|
writeFileSync: writeFileSync$4,
|
|
@@ -1122,10 +1122,10 @@ const {
|
|
|
1122
1122
|
lstatSync,
|
|
1123
1123
|
} = require$$0$2;
|
|
1124
1124
|
|
|
1125
|
-
function ensureDirectoryExistence$
|
|
1125
|
+
function ensureDirectoryExistence$1(filePath) {
|
|
1126
1126
|
try {
|
|
1127
1127
|
// isDirectory
|
|
1128
|
-
var dirname = path$
|
|
1128
|
+
var dirname = path$m.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.
|
|
@@ -1133,7 +1133,7 @@ function ensureDirectoryExistence$2(filePath) {
|
|
|
1133
1133
|
return true;
|
|
1134
1134
|
}
|
|
1135
1135
|
// recursion...
|
|
1136
|
-
ensureDirectoryExistence$
|
|
1136
|
+
ensureDirectoryExistence$1(dirname);
|
|
1137
1137
|
mkdirSync(dirname);
|
|
1138
1138
|
} catch (e) {
|
|
1139
1139
|
console.log("ensure directory " + e.message);
|
|
@@ -1179,7 +1179,7 @@ function checkDirectory$1(dir) {
|
|
|
1179
1179
|
function getFileContents$8(filepath, defaultReturn = []) {
|
|
1180
1180
|
try {
|
|
1181
1181
|
// lets first make sure all is there...
|
|
1182
|
-
ensureDirectoryExistence$
|
|
1182
|
+
ensureDirectoryExistence$1(filepath);
|
|
1183
1183
|
|
|
1184
1184
|
// and now lets read the file...
|
|
1185
1185
|
let fileContents = JSON.stringify(defaultReturn);
|
|
@@ -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$
|
|
1243
|
+
unlinkSync(path$m.join(directory, file), (err) => {
|
|
1244
1244
|
if (err) throw err;
|
|
1245
1245
|
});
|
|
1246
1246
|
}
|
|
@@ -1250,15 +1250,15 @@ function removeFilesFromDirectory(directory, excludeFiles = []) {
|
|
|
1250
1250
|
}
|
|
1251
1251
|
|
|
1252
1252
|
var file = {
|
|
1253
|
-
ensureDirectoryExistence: ensureDirectoryExistence$
|
|
1253
|
+
ensureDirectoryExistence: ensureDirectoryExistence$1,
|
|
1254
1254
|
getFileContents: getFileContents$8,
|
|
1255
1255
|
writeToFile: writeToFile$3,
|
|
1256
1256
|
removeFilesFromDirectory,
|
|
1257
1257
|
checkDirectory: checkDirectory$1,
|
|
1258
1258
|
};
|
|
1259
1259
|
|
|
1260
|
-
const { app: app$
|
|
1261
|
-
const path$
|
|
1260
|
+
const { app: app$e } = require$$0$1;
|
|
1261
|
+
const path$l = 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$
|
|
1309
|
-
app$
|
|
1308
|
+
const filename = path$l.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$
|
|
1358
|
-
app$
|
|
1357
|
+
const filename = path$l.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$
|
|
1407
|
-
app$
|
|
1406
|
+
const filename = path$l.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$
|
|
1441
|
-
app$
|
|
1440
|
+
const filename = path$l.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$
|
|
1469
|
-
app$
|
|
1468
|
+
const filename = path$l.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$
|
|
1513
|
-
const path$
|
|
1512
|
+
const { app: app$d } = require$$0$1;
|
|
1513
|
+
const path$k = 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$
|
|
1534
|
-
app$
|
|
1533
|
+
const filename = path$k.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$
|
|
1580
|
-
app$
|
|
1579
|
+
const filename = path$k.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$
|
|
1622
|
-
app$
|
|
1621
|
+
const filename = path$k.join(
|
|
1622
|
+
app$d.getPath("userData"),
|
|
1623
1623
|
appName$6,
|
|
1624
1624
|
appId,
|
|
1625
1625
|
configFilename$4,
|
|
@@ -1695,9 +1695,9 @@ var themeController_1 = themeController$5;
|
|
|
1695
1695
|
* `/data/`.
|
|
1696
1696
|
*/
|
|
1697
1697
|
|
|
1698
|
-
const path$
|
|
1699
|
-
const fs$
|
|
1700
|
-
const { app: app$
|
|
1698
|
+
const path$j = require$$1$2;
|
|
1699
|
+
const fs$e = require$$0$2;
|
|
1700
|
+
const { app: app$c } = require$$0$1;
|
|
1701
1701
|
|
|
1702
1702
|
const APP_NAME = "Dashboard";
|
|
1703
1703
|
|
|
@@ -1706,10 +1706,10 @@ const APP_NAME = "Dashboard";
|
|
|
1706
1706
|
* @returns {string[]} ordered allowed roots for that category
|
|
1707
1707
|
*/
|
|
1708
1708
|
function getAllowedRoots$2(category) {
|
|
1709
|
-
const userData = app$
|
|
1709
|
+
const userData = app$c.getPath("userData");
|
|
1710
1710
|
switch (category) {
|
|
1711
1711
|
case "data": {
|
|
1712
|
-
const def = path$
|
|
1712
|
+
const def = path$j.join(userData, APP_NAME, "data");
|
|
1713
1713
|
// The user can configure a custom data directory in
|
|
1714
1714
|
// Settings → General → Data Directory. If set, that
|
|
1715
1715
|
// location is ALSO an allowed root. We don't replace the
|
|
@@ -1719,13 +1719,13 @@ function getAllowedRoots$2(category) {
|
|
|
1719
1719
|
return override ? [def, override] : [def];
|
|
1720
1720
|
}
|
|
1721
1721
|
case "themes":
|
|
1722
|
-
return [path$
|
|
1722
|
+
return [path$j.join(userData, APP_NAME, "themes")];
|
|
1723
1723
|
case "widgets":
|
|
1724
|
-
return [path$
|
|
1724
|
+
return [path$j.join(userData, "widgets")];
|
|
1725
1725
|
case "plugins":
|
|
1726
|
-
return [path$
|
|
1726
|
+
return [path$j.join(userData, "plugins")];
|
|
1727
1727
|
case "downloads":
|
|
1728
|
-
return [app$
|
|
1728
|
+
return [app$c.getPath("downloads")];
|
|
1729
1729
|
default:
|
|
1730
1730
|
throw new Error("safePath: unknown allowed-roots category: " + category);
|
|
1731
1731
|
}
|
|
@@ -1740,13 +1740,13 @@ function getAllowedRoots$2(category) {
|
|
|
1740
1740
|
*/
|
|
1741
1741
|
function readDataDirectoryFromSettings() {
|
|
1742
1742
|
try {
|
|
1743
|
-
const settingsPath = path$
|
|
1744
|
-
app$
|
|
1743
|
+
const settingsPath = path$j.join(
|
|
1744
|
+
app$c.getPath("userData"),
|
|
1745
1745
|
APP_NAME,
|
|
1746
1746
|
"settings.json",
|
|
1747
1747
|
);
|
|
1748
|
-
if (!fs$
|
|
1749
|
-
const raw = fs$
|
|
1748
|
+
if (!fs$e.existsSync(settingsPath)) return undefined;
|
|
1749
|
+
const raw = fs$e.readFileSync(settingsPath, "utf8");
|
|
1750
1750
|
const settings = JSON.parse(raw);
|
|
1751
1751
|
const dir = settings && settings.dataDirectory;
|
|
1752
1752
|
if (typeof dir === "string" && dir) return dir;
|
|
@@ -1772,18 +1772,18 @@ function safePath$3(requested, allowedRoots) {
|
|
|
1772
1772
|
throw new Error("safePath: allowedRoots must be a non-empty array");
|
|
1773
1773
|
}
|
|
1774
1774
|
|
|
1775
|
-
const resolved = path$
|
|
1775
|
+
const resolved = path$j.resolve(requested);
|
|
1776
1776
|
|
|
1777
1777
|
// Real-path through symlinks. If the file doesn't exist yet (a
|
|
1778
1778
|
// create-new operation), real-path the parent so a symlink in the
|
|
1779
1779
|
// parent chain can't trick us.
|
|
1780
1780
|
let real = resolved;
|
|
1781
1781
|
try {
|
|
1782
|
-
real = fs$
|
|
1782
|
+
real = fs$e.realpathSync(resolved);
|
|
1783
1783
|
} catch (_e) {
|
|
1784
1784
|
try {
|
|
1785
|
-
const parent = fs$
|
|
1786
|
-
real = path$
|
|
1785
|
+
const parent = fs$e.realpathSync(path$j.dirname(resolved));
|
|
1786
|
+
real = path$j.join(parent, path$j.basename(resolved));
|
|
1787
1787
|
} catch (_e2) {
|
|
1788
1788
|
// Parent doesn't exist either. Use the resolved-but-not-
|
|
1789
1789
|
// real path; the caller's mkdirSync will happen inside the
|
|
@@ -1795,14 +1795,14 @@ function safePath$3(requested, allowedRoots) {
|
|
|
1795
1795
|
for (const root of allowedRoots) {
|
|
1796
1796
|
let realRoot = root;
|
|
1797
1797
|
try {
|
|
1798
|
-
if (fs$
|
|
1798
|
+
if (fs$e.existsSync(root)) realRoot = fs$e.realpathSync(root);
|
|
1799
1799
|
} catch (_e) {
|
|
1800
1800
|
// root doesn't exist or isn't reachable — keep as-is for
|
|
1801
1801
|
// the comparison below
|
|
1802
1802
|
}
|
|
1803
1803
|
// Exact match OR strictly-inside (with separator to prevent
|
|
1804
1804
|
// /data-evil/ matching /data/).
|
|
1805
|
-
if (real === realRoot || real.startsWith(realRoot + path$
|
|
1805
|
+
if (real === realRoot || real.startsWith(realRoot + path$j.sep)) {
|
|
1806
1806
|
return real;
|
|
1807
1807
|
}
|
|
1808
1808
|
}
|
|
@@ -1823,6 +1823,795 @@ var safePath_1 = {
|
|
|
1823
1823
|
getAllowedRoots: getAllowedRoots$2,
|
|
1824
1824
|
};
|
|
1825
1825
|
|
|
1826
|
+
/**
|
|
1827
|
+
* grantedPermissions.js
|
|
1828
|
+
*
|
|
1829
|
+
* Stores the user's actual MCP permission grants per widget. This is the
|
|
1830
|
+
* Slice-2 enforcement source of truth — separate from the widget's declared
|
|
1831
|
+
* `dash.permissions.mcp` block (which is just a request).
|
|
1832
|
+
*
|
|
1833
|
+
* The runtime gate (permissionGate.gateToolCall) reads from here only.
|
|
1834
|
+
* A widget with a declared manifest but no grant entry has no access:
|
|
1835
|
+
* fail-closed. The user grants permissions at install time (consent modal)
|
|
1836
|
+
* or later in Settings → Privacy & Security.
|
|
1837
|
+
*
|
|
1838
|
+
* Storage: userData/widgetMcpGrants.json. Atomic writes via tmp + rename.
|
|
1839
|
+
*
|
|
1840
|
+
* Shape on disk:
|
|
1841
|
+
* {
|
|
1842
|
+
* "@trops/notes-summarizer": {
|
|
1843
|
+
* "servers": {
|
|
1844
|
+
* "filesystem": {
|
|
1845
|
+
* "tools": ["read_file"],
|
|
1846
|
+
* "readPaths": ["/Users/jane/Documents/notes"],
|
|
1847
|
+
* "writePaths": []
|
|
1848
|
+
* }
|
|
1849
|
+
* }
|
|
1850
|
+
* }
|
|
1851
|
+
* }
|
|
1852
|
+
*
|
|
1853
|
+
* Note: paths are stored as-is (already tilde-expanded by the manifest
|
|
1854
|
+
* parser before grants are written). Tests can re-expand via
|
|
1855
|
+
* widgetPermissions.expandHome if they store ~ literals.
|
|
1856
|
+
*
|
|
1857
|
+
* Public API:
|
|
1858
|
+
* getGrant(widgetId) → grant | null
|
|
1859
|
+
* setGrant(widgetId, perms) → boolean
|
|
1860
|
+
* revokeGrant(widgetId) → boolean
|
|
1861
|
+
* revokeServer(widgetId, serverName) → boolean
|
|
1862
|
+
* listAllGrants() → [{ widgetId, granted }]
|
|
1863
|
+
* clearCache() → void // test-only
|
|
1864
|
+
*/
|
|
1865
|
+
|
|
1866
|
+
const fs$d = require$$0$2;
|
|
1867
|
+
const path$i = require$$1$2;
|
|
1868
|
+
const { app: app$b } = require$$0$1;
|
|
1869
|
+
|
|
1870
|
+
const FILE_NAME = "widgetMcpGrants.json";
|
|
1871
|
+
|
|
1872
|
+
// In-process cache of the entire grants file. Lazily loaded; invalidated
|
|
1873
|
+
// on every write.
|
|
1874
|
+
let _cache$1 = null;
|
|
1875
|
+
|
|
1876
|
+
function grantsFilePath() {
|
|
1877
|
+
return path$i.join(app$b.getPath("userData"), FILE_NAME);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function loadFromDisk() {
|
|
1881
|
+
const p = grantsFilePath();
|
|
1882
|
+
if (!fs$d.existsSync(p)) return {};
|
|
1883
|
+
try {
|
|
1884
|
+
const raw = fs$d.readFileSync(p, "utf8");
|
|
1885
|
+
const parsed = JSON.parse(raw);
|
|
1886
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
1887
|
+
return parsed;
|
|
1888
|
+
} catch (e) {
|
|
1889
|
+
console.warn("[grantedPermissions] failed to read " + p + ": " + e.message);
|
|
1890
|
+
return {};
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function ensureCache() {
|
|
1895
|
+
if (_cache$1 === null) _cache$1 = loadFromDisk();
|
|
1896
|
+
return _cache$1;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function writeToDisk(data) {
|
|
1900
|
+
const p = grantsFilePath();
|
|
1901
|
+
const tmp = p + ".tmp";
|
|
1902
|
+
// Ensure parent dir exists (userData should already, but be defensive
|
|
1903
|
+
// for first-launch / freshly-cleared profile cases).
|
|
1904
|
+
fs$d.mkdirSync(path$i.dirname(p), { recursive: true });
|
|
1905
|
+
fs$d.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
1906
|
+
fs$d.renameSync(tmp, p);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// Recognized origins for a persisted grant.
|
|
1910
|
+
// "declared" — user approved against the developer's declared
|
|
1911
|
+
// dash.permissions.mcp block at install time.
|
|
1912
|
+
// "discovered" — install-time scanner produced a synthetic manifest
|
|
1913
|
+
// the user approved.
|
|
1914
|
+
// "manual" — user typed entries themselves in
|
|
1915
|
+
// Settings → Privacy & Security with no manifest backing.
|
|
1916
|
+
// "live" — user approved a just-in-time consent prompt at
|
|
1917
|
+
// runtime when a tool call hit the gate without a
|
|
1918
|
+
// matching grant.
|
|
1919
|
+
// Other values are dropped on persist (legacy grants stay null).
|
|
1920
|
+
const ALLOWED_GRANT_ORIGINS = new Set([
|
|
1921
|
+
"declared",
|
|
1922
|
+
"discovered",
|
|
1923
|
+
"manual",
|
|
1924
|
+
"live",
|
|
1925
|
+
]);
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Sanitize a perms object before persisting. Drops unknown keys, coerces
|
|
1929
|
+
* arrays of strings, and silently ignores malformed servers. Mirrors the
|
|
1930
|
+
* shape produced by parseManifestPermissions so the gate reads either
|
|
1931
|
+
* declared or granted with the same code path.
|
|
1932
|
+
*
|
|
1933
|
+
* Optional `grantOrigin` field is preserved when it's one of the
|
|
1934
|
+
* recognized values; bogus values are dropped.
|
|
1935
|
+
*
|
|
1936
|
+
* Phase 2 (JIT consent for non-MCP domains): also accepts a top-level
|
|
1937
|
+
* `domains` block. Each known domain has its own shape — `domains.fs`
|
|
1938
|
+
* has `readPaths`/`writePaths`, future domains will have their own.
|
|
1939
|
+
* The `servers` block (MCP) and `domains` block coexist on the same
|
|
1940
|
+
* grant; either or both may be present.
|
|
1941
|
+
*
|
|
1942
|
+
* Either `servers` (MCP) or any `domains.*` block is enough to make the
|
|
1943
|
+
* grant non-empty. If neither is present, the grant is rejected as
|
|
1944
|
+
* malformed.
|
|
1945
|
+
*/
|
|
1946
|
+
function sanitizePerms(perms) {
|
|
1947
|
+
if (!perms || typeof perms !== "object") return null;
|
|
1948
|
+
const rawServers =
|
|
1949
|
+
perms.servers && typeof perms.servers === "object" ? perms.servers : null;
|
|
1950
|
+
const rawDomains =
|
|
1951
|
+
perms.domains && typeof perms.domains === "object" ? perms.domains : null;
|
|
1952
|
+
if (!rawServers && !rawDomains) return null;
|
|
1953
|
+
|
|
1954
|
+
const out = {};
|
|
1955
|
+
|
|
1956
|
+
if (rawServers) {
|
|
1957
|
+
const servers = {};
|
|
1958
|
+
for (const [name, raw] of Object.entries(rawServers)) {
|
|
1959
|
+
if (!raw || typeof raw !== "object") continue;
|
|
1960
|
+
servers[name] = {
|
|
1961
|
+
tools: Array.isArray(raw.tools)
|
|
1962
|
+
? raw.tools.filter((t) => typeof t === "string")
|
|
1963
|
+
: [],
|
|
1964
|
+
readPaths: Array.isArray(raw.readPaths)
|
|
1965
|
+
? raw.readPaths.filter((p) => typeof p === "string")
|
|
1966
|
+
: [],
|
|
1967
|
+
writePaths: Array.isArray(raw.writePaths)
|
|
1968
|
+
? raw.writePaths.filter((p) => typeof p === "string")
|
|
1969
|
+
: [],
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
out.servers = servers;
|
|
1973
|
+
} else {
|
|
1974
|
+
// Always emit `servers` so consumers don't have to null-check it.
|
|
1975
|
+
out.servers = {};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
if (rawDomains) {
|
|
1979
|
+
const domains = {};
|
|
1980
|
+
for (const [name, raw] of Object.entries(rawDomains)) {
|
|
1981
|
+
if (!raw || typeof raw !== "object") continue;
|
|
1982
|
+
if (name === "fs") {
|
|
1983
|
+
domains.fs = {
|
|
1984
|
+
readPaths: Array.isArray(raw.readPaths)
|
|
1985
|
+
? raw.readPaths.filter((p) => typeof p === "string")
|
|
1986
|
+
: [],
|
|
1987
|
+
writePaths: Array.isArray(raw.writePaths)
|
|
1988
|
+
? raw.writePaths.filter((p) => typeof p === "string")
|
|
1989
|
+
: [],
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
// Future domains plug in here. Unknown domain names are dropped.
|
|
1993
|
+
}
|
|
1994
|
+
if (Object.keys(domains).length > 0) {
|
|
1995
|
+
out.domains = domains;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
if (
|
|
2000
|
+
typeof perms.grantOrigin === "string" &&
|
|
2001
|
+
ALLOWED_GRANT_ORIGINS.has(perms.grantOrigin)
|
|
2002
|
+
) {
|
|
2003
|
+
out.grantOrigin = perms.grantOrigin;
|
|
2004
|
+
}
|
|
2005
|
+
return out;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
function getGrant$3(widgetId) {
|
|
2009
|
+
if (typeof widgetId !== "string" || !widgetId) return null;
|
|
2010
|
+
const all = ensureCache();
|
|
2011
|
+
return all[widgetId] || null;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function setGrant$3(widgetId, perms) {
|
|
2015
|
+
if (typeof widgetId !== "string" || !widgetId) return false;
|
|
2016
|
+
const sanitized = sanitizePerms(perms);
|
|
2017
|
+
if (!sanitized) return false;
|
|
2018
|
+
const all = ensureCache();
|
|
2019
|
+
all[widgetId] = sanitized;
|
|
2020
|
+
try {
|
|
2021
|
+
writeToDisk(all);
|
|
2022
|
+
return true;
|
|
2023
|
+
} catch (e) {
|
|
2024
|
+
console.warn(
|
|
2025
|
+
"[grantedPermissions] failed to write grant for " +
|
|
2026
|
+
widgetId +
|
|
2027
|
+
": " +
|
|
2028
|
+
e.message,
|
|
2029
|
+
);
|
|
2030
|
+
// Roll back the cache entry so memory matches disk.
|
|
2031
|
+
_cache$1 = loadFromDisk();
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function revokeGrant$1(widgetId) {
|
|
2037
|
+
if (typeof widgetId !== "string" || !widgetId) return false;
|
|
2038
|
+
const all = ensureCache();
|
|
2039
|
+
if (!Object.prototype.hasOwnProperty.call(all, widgetId)) return false;
|
|
2040
|
+
delete all[widgetId];
|
|
2041
|
+
try {
|
|
2042
|
+
writeToDisk(all);
|
|
2043
|
+
return true;
|
|
2044
|
+
} catch (e) {
|
|
2045
|
+
console.warn(
|
|
2046
|
+
"[grantedPermissions] failed to revoke grant for " +
|
|
2047
|
+
widgetId +
|
|
2048
|
+
": " +
|
|
2049
|
+
e.message,
|
|
2050
|
+
);
|
|
2051
|
+
_cache$1 = loadFromDisk();
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function revokeServer$1(widgetId, serverName) {
|
|
2057
|
+
if (typeof widgetId !== "string" || !widgetId) return false;
|
|
2058
|
+
if (typeof serverName !== "string" || !serverName) return false;
|
|
2059
|
+
const all = ensureCache();
|
|
2060
|
+
const widgetEntry = all[widgetId];
|
|
2061
|
+
if (!widgetEntry || !widgetEntry.servers) return false;
|
|
2062
|
+
if (!Object.prototype.hasOwnProperty.call(widgetEntry.servers, serverName))
|
|
2063
|
+
return false;
|
|
2064
|
+
delete widgetEntry.servers[serverName];
|
|
2065
|
+
try {
|
|
2066
|
+
writeToDisk(all);
|
|
2067
|
+
return true;
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
console.warn(
|
|
2070
|
+
"[grantedPermissions] failed to revoke server " +
|
|
2071
|
+
serverName +
|
|
2072
|
+
" for " +
|
|
2073
|
+
widgetId +
|
|
2074
|
+
": " +
|
|
2075
|
+
e.message,
|
|
2076
|
+
);
|
|
2077
|
+
_cache$1 = loadFromDisk();
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function listAllGrants$1() {
|
|
2083
|
+
const all = ensureCache();
|
|
2084
|
+
return Object.entries(all).map(([widgetId, granted]) => ({
|
|
2085
|
+
widgetId,
|
|
2086
|
+
granted,
|
|
2087
|
+
}));
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function clearCache$1() {
|
|
2091
|
+
_cache$1 = null;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
var grantedPermissions = {
|
|
2095
|
+
getGrant: getGrant$3,
|
|
2096
|
+
setGrant: setGrant$3,
|
|
2097
|
+
revokeGrant: revokeGrant$1,
|
|
2098
|
+
revokeServer: revokeServer$1,
|
|
2099
|
+
listAllGrants: listAllGrants$1,
|
|
2100
|
+
clearCache: clearCache$1,
|
|
2101
|
+
ALLOWED_GRANT_ORIGINS,
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* jitConsent.js
|
|
2106
|
+
*
|
|
2107
|
+
* Just-in-time permission consent for widget→backend calls.
|
|
2108
|
+
*
|
|
2109
|
+
* When a widget hits a gate without an existing grant for the requested
|
|
2110
|
+
* (domain, action, args), the gate calls `requestApproval` which:
|
|
2111
|
+
* 1. Synchronously emits `widget:permission-required` to all
|
|
2112
|
+
* BrowserWindows with a unique requestId.
|
|
2113
|
+
* 2. Returns a Promise that resolves on user response or rejects on
|
|
2114
|
+
* timeout.
|
|
2115
|
+
* 3. Coalesces requests with the same coalescing key so a widget
|
|
2116
|
+
* bursting identical calls produces one prompt, not many.
|
|
2117
|
+
*
|
|
2118
|
+
* The renderer's JitConsentModal subscribes to the event, presents the
|
|
2119
|
+
* user with granularity options (this once / this tool / this tool +
|
|
2120
|
+
* parent dir), and replies via `widget:permission-response` with
|
|
2121
|
+
* `{ requestId, decision }`. main.js wires the IPC handler back to
|
|
2122
|
+
* `_handleResponse`.
|
|
2123
|
+
*
|
|
2124
|
+
* The module is intentionally domain-agnostic in shape — the request
|
|
2125
|
+
* payload carries `domain` so future plug-ins (fs, algolia, llm) reuse
|
|
2126
|
+
* the same machinery. Phase 1 only emits with `domain: "mcp"`.
|
|
2127
|
+
*
|
|
2128
|
+
* Public surface:
|
|
2129
|
+
* requestApproval(req, opts) → Promise<{ approve, scope?, ... }>
|
|
2130
|
+
* _handleResponse({ requestId, decision }) → void (called from main.js IPC)
|
|
2131
|
+
* _resetForTest() → void (test-only)
|
|
2132
|
+
*/
|
|
2133
|
+
|
|
2134
|
+
const { BrowserWindow: BrowserWindow$2, ipcMain: ipcMain$2 } = require$$0$1;
|
|
2135
|
+
|
|
2136
|
+
const REQUEST_CHANNEL = "widget:permission-required";
|
|
2137
|
+
const RESPONSE_CHANNEL = "widget:permission-response";
|
|
2138
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
2139
|
+
|
|
2140
|
+
// requestId → { resolve, reject, timeout, coalesceKey, joinedResolvers }
|
|
2141
|
+
const _pending = new Map();
|
|
2142
|
+
// coalesceKey → requestId (so duplicate requests join the live one)
|
|
2143
|
+
const _coalesce = new Map();
|
|
2144
|
+
let _idCounter = 0;
|
|
2145
|
+
|
|
2146
|
+
function nextRequestId() {
|
|
2147
|
+
_idCounter += 1;
|
|
2148
|
+
return `jit-${Date.now()}-${_idCounter}`;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
/**
|
|
2152
|
+
* Build a coalescing key from the request. Two requests share the same
|
|
2153
|
+
* key iff they're "the same prompt" — same widget, same domain+action,
|
|
2154
|
+
* same target server/tool. Args beyond that (e.g. exact path) DON'T
|
|
2155
|
+
* differentiate; if the user is being asked about read_file already,
|
|
2156
|
+
* approving handles all current paths.
|
|
2157
|
+
*/
|
|
2158
|
+
function coalesceKeyOf(req) {
|
|
2159
|
+
if (req.domain === "mcp") {
|
|
2160
|
+
const innerArgs = req.args || {};
|
|
2161
|
+
return [
|
|
2162
|
+
req.widgetId,
|
|
2163
|
+
"mcp",
|
|
2164
|
+
innerArgs.serverName || "",
|
|
2165
|
+
innerArgs.toolName || "",
|
|
2166
|
+
].join("::");
|
|
2167
|
+
}
|
|
2168
|
+
// Default: domain + action + serialized top-level args
|
|
2169
|
+
return [
|
|
2170
|
+
req.widgetId,
|
|
2171
|
+
req.domain,
|
|
2172
|
+
req.action,
|
|
2173
|
+
JSON.stringify(req.args || {}),
|
|
2174
|
+
].join("::");
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function emitEvent(payload) {
|
|
2178
|
+
let wins = [];
|
|
2179
|
+
try {
|
|
2180
|
+
wins = BrowserWindow$2.getAllWindows() || [];
|
|
2181
|
+
} catch {
|
|
2182
|
+
wins = [];
|
|
2183
|
+
}
|
|
2184
|
+
for (const w of wins) {
|
|
2185
|
+
try {
|
|
2186
|
+
w?.webContents?.send?.(REQUEST_CHANNEL, payload);
|
|
2187
|
+
} catch {
|
|
2188
|
+
// best-effort broadcast
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function validateRequest(req) {
|
|
2194
|
+
if (!req || typeof req !== "object") return "invalid request: not an object";
|
|
2195
|
+
if (typeof req.widgetId !== "string" || !req.widgetId)
|
|
2196
|
+
return "invalid request: widgetId required";
|
|
2197
|
+
if (typeof req.domain !== "string" || !req.domain)
|
|
2198
|
+
return "invalid request: domain required";
|
|
2199
|
+
if (typeof req.action !== "string" || !req.action)
|
|
2200
|
+
return "invalid request: action required";
|
|
2201
|
+
return null;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
/**
|
|
2205
|
+
* Request user approval for an out-of-grant call. Returns a promise
|
|
2206
|
+
* that resolves with the user's decision or rejects on timeout / bad
|
|
2207
|
+
* input.
|
|
2208
|
+
*
|
|
2209
|
+
* decision shape (resolved value):
|
|
2210
|
+
* { approve: true, scope: "once" | "tool" | "parent" | "custom", ...extras }
|
|
2211
|
+
* { approve: false, reason?: string }
|
|
2212
|
+
*
|
|
2213
|
+
* `scope` informs the caller how to write the resulting grant.
|
|
2214
|
+
*/
|
|
2215
|
+
function requestApproval$2(req, opts = {}) {
|
|
2216
|
+
const validation = validateRequest(req);
|
|
2217
|
+
if (validation) {
|
|
2218
|
+
return Promise.reject(new Error(validation));
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs)
|
|
2222
|
+
? opts.timeoutMs
|
|
2223
|
+
: DEFAULT_TIMEOUT_MS;
|
|
2224
|
+
|
|
2225
|
+
// If a prompt for the same coalesce key is already pending, join it.
|
|
2226
|
+
const key = coalesceKeyOf(req);
|
|
2227
|
+
if (_coalesce.has(key)) {
|
|
2228
|
+
const existingId = _coalesce.get(key);
|
|
2229
|
+
const existing = _pending.get(existingId);
|
|
2230
|
+
if (existing) {
|
|
2231
|
+
return new Promise((resolve, reject) => {
|
|
2232
|
+
existing.joinedResolvers.push({ resolve, reject });
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
// Stale coalesce entry; drop and fall through to a fresh request.
|
|
2236
|
+
_coalesce.delete(key);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
return new Promise((resolve, reject) => {
|
|
2240
|
+
const requestId = nextRequestId();
|
|
2241
|
+
const timeout = setTimeout(() => {
|
|
2242
|
+
const entry = _pending.get(requestId);
|
|
2243
|
+
if (!entry) return;
|
|
2244
|
+
_pending.delete(requestId);
|
|
2245
|
+
_coalesce.delete(entry.coalesceKey);
|
|
2246
|
+
const err = new Error(
|
|
2247
|
+
`JIT consent timed out for ${req.widgetId} (${req.domain}/${req.action}) after ${timeoutMs}ms`,
|
|
2248
|
+
);
|
|
2249
|
+
reject(err);
|
|
2250
|
+
for (const j of entry.joinedResolvers) j.reject(err);
|
|
2251
|
+
}, timeoutMs);
|
|
2252
|
+
|
|
2253
|
+
_pending.set(requestId, {
|
|
2254
|
+
resolve,
|
|
2255
|
+
reject,
|
|
2256
|
+
timeout,
|
|
2257
|
+
coalesceKey: key,
|
|
2258
|
+
joinedResolvers: [],
|
|
2259
|
+
});
|
|
2260
|
+
_coalesce.set(key, requestId);
|
|
2261
|
+
|
|
2262
|
+
emitEvent({
|
|
2263
|
+
requestId,
|
|
2264
|
+
widgetId: req.widgetId,
|
|
2265
|
+
domain: req.domain,
|
|
2266
|
+
action: req.action,
|
|
2267
|
+
args: req.args || {},
|
|
2268
|
+
});
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
function _handleResponse({ requestId, decision } = {}) {
|
|
2273
|
+
if (!requestId || typeof requestId !== "string") return;
|
|
2274
|
+
const entry = _pending.get(requestId);
|
|
2275
|
+
if (!entry) return; // unknown request — drop silently
|
|
2276
|
+
clearTimeout(entry.timeout);
|
|
2277
|
+
_pending.delete(requestId);
|
|
2278
|
+
_coalesce.delete(entry.coalesceKey);
|
|
2279
|
+
const safe =
|
|
2280
|
+
decision && typeof decision === "object" ? decision : { approve: false };
|
|
2281
|
+
entry.resolve(safe);
|
|
2282
|
+
for (const j of entry.joinedResolvers) j.resolve(safe);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function _resetForTest() {
|
|
2286
|
+
for (const entry of _pending.values()) clearTimeout(entry.timeout);
|
|
2287
|
+
_pending.clear();
|
|
2288
|
+
_coalesce.clear();
|
|
2289
|
+
_idCounter = 0;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
let _handlersRegistered = false;
|
|
2293
|
+
/**
|
|
2294
|
+
* Wire the renderer→main response IPC. Idempotent.
|
|
2295
|
+
* Call once from main.js alongside other ipcMain setup.
|
|
2296
|
+
*/
|
|
2297
|
+
function setupJitConsentHandlers() {
|
|
2298
|
+
if (_handlersRegistered) return;
|
|
2299
|
+
if (!ipcMain$2 || typeof ipcMain$2.on !== "function") return;
|
|
2300
|
+
ipcMain$2.on(RESPONSE_CHANNEL, (_event, payload) => {
|
|
2301
|
+
_handleResponse(payload);
|
|
2302
|
+
});
|
|
2303
|
+
_handlersRegistered = true;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
var jitConsent$1 = {
|
|
2307
|
+
requestApproval: requestApproval$2,
|
|
2308
|
+
setupJitConsentHandlers,
|
|
2309
|
+
_handleResponse,
|
|
2310
|
+
_resetForTest,
|
|
2311
|
+
REQUEST_CHANNEL,
|
|
2312
|
+
RESPONSE_CHANNEL,
|
|
2313
|
+
DEFAULT_TIMEOUT_MS,
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
/**
|
|
2317
|
+
* fsGate.js
|
|
2318
|
+
*
|
|
2319
|
+
* Per-widget gate for `mainApi.data.*` IPC handlers (Phase 2 of JIT
|
|
2320
|
+
* consent). Same shape as `electron/mcp/permissionGate.js` but for the
|
|
2321
|
+
* filesystem domain — saveToFile/readFromFile in dataController.
|
|
2322
|
+
*
|
|
2323
|
+
* The gate evaluates against the widget's persisted grant under
|
|
2324
|
+
* `grant.domains.fs.{readPaths,writePaths}`. The existing `safePath()`
|
|
2325
|
+
* containment in dataController is unchanged — that constrains paths
|
|
2326
|
+
* to the userData/Dashboard/data dir; this gate adds per-widget
|
|
2327
|
+
* identity scoping on top, so two widgets can't read each other's
|
|
2328
|
+
* files even though both are inside the data dir.
|
|
2329
|
+
*
|
|
2330
|
+
* Action → read/write classification by name. Read tools may match
|
|
2331
|
+
* either readPaths or writePaths (write access implies read); write
|
|
2332
|
+
* tools must match writePaths only.
|
|
2333
|
+
*
|
|
2334
|
+
* Filename matching:
|
|
2335
|
+
* - Exact match
|
|
2336
|
+
* - "*" wildcard in the grant matches any filename (escape hatch
|
|
2337
|
+
* for users who want broad widget access; surfaced in the JIT
|
|
2338
|
+
* modal as "no path scope — risky")
|
|
2339
|
+
*
|
|
2340
|
+
* Path-traversal protection lives in safePath; the gate doesn't
|
|
2341
|
+
* re-check it. The gate is purely an identity+filename allowlist on
|
|
2342
|
+
* top of safePath's containment.
|
|
2343
|
+
*
|
|
2344
|
+
* JIT escalation: when the runtime calls `gateFsCallWithJit` and the
|
|
2345
|
+
* gate denies for "no fs permissions granted", a permission-required
|
|
2346
|
+
* IPC fires and the user's response is merged into the persisted
|
|
2347
|
+
* grant. Other denial reasons (filename not in allowlist, write to a
|
|
2348
|
+
* read-only entry) stay synchronous.
|
|
2349
|
+
*/
|
|
2350
|
+
|
|
2351
|
+
const { getGrant: getGrant$2, setGrant: setGrant$2 } = grantedPermissions;
|
|
2352
|
+
const { requestApproval: requestApproval$1 } = jitConsent$1;
|
|
2353
|
+
|
|
2354
|
+
// Action names treated as writes. Anything not in this set is a read.
|
|
2355
|
+
// Conservative — when in doubt, classify as a read so write-protected
|
|
2356
|
+
// grants don't accidentally allow writes.
|
|
2357
|
+
const WRITE_ACTIONS = new Set([
|
|
2358
|
+
"saveToFile",
|
|
2359
|
+
"saveData", // future-proof: the renderer-facing API name
|
|
2360
|
+
"convertJsonToCsvFile",
|
|
2361
|
+
"parseXMLStream",
|
|
2362
|
+
"parseCSVStream",
|
|
2363
|
+
"readDataFromURL", // writes to toFilepath despite the name
|
|
2364
|
+
"transformFile",
|
|
2365
|
+
]);
|
|
2366
|
+
|
|
2367
|
+
function isFsWriteAction(action) {
|
|
2368
|
+
return WRITE_ACTIONS.has(action);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
function _isNoGrantDenial$1(reason) {
|
|
2372
|
+
return (
|
|
2373
|
+
typeof reason === "string" && /no fs permissions granted/i.test(reason)
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function _filenameMatches(filename, allowedList) {
|
|
2378
|
+
if (!Array.isArray(allowedList) || allowedList.length === 0) return false;
|
|
2379
|
+
if (allowedList.includes("*")) return true;
|
|
2380
|
+
return allowedList.includes(filename);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Synchronous gate evaluation.
|
|
2385
|
+
* @returns {{ allow: true } | { allow: false, reason: string }}
|
|
2386
|
+
*/
|
|
2387
|
+
function gateFsCall$1({ widgetId, action, args }) {
|
|
2388
|
+
if (!widgetId) {
|
|
2389
|
+
return {
|
|
2390
|
+
allow: false,
|
|
2391
|
+
reason: "no widgetId supplied; cannot determine fs permissions",
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
const filename = args && typeof args === "object" ? args.filename : null;
|
|
2396
|
+
if (typeof filename !== "string" || !filename) {
|
|
2397
|
+
return {
|
|
2398
|
+
allow: false,
|
|
2399
|
+
reason:
|
|
2400
|
+
"fs gate: action '" +
|
|
2401
|
+
action +
|
|
2402
|
+
"' requires args.filename (got: " +
|
|
2403
|
+
JSON.stringify(filename) +
|
|
2404
|
+
")",
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
const grant = getGrant$2(widgetId);
|
|
2409
|
+
const fsPerms = grant && grant.domains && grant.domains.fs;
|
|
2410
|
+
if (!fsPerms) {
|
|
2411
|
+
return {
|
|
2412
|
+
allow: false,
|
|
2413
|
+
reason:
|
|
2414
|
+
"widget '" +
|
|
2415
|
+
widgetId +
|
|
2416
|
+
"' has no fs permissions granted; user must approve at runtime or in Settings → Privacy & Security",
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
const isWrite = isFsWriteAction(action);
|
|
2421
|
+
|
|
2422
|
+
if (isWrite) {
|
|
2423
|
+
if (!Array.isArray(fsPerms.writePaths) || fsPerms.writePaths.length === 0) {
|
|
2424
|
+
return {
|
|
2425
|
+
allow: false,
|
|
2426
|
+
reason:
|
|
2427
|
+
"fs gate: widget '" +
|
|
2428
|
+
widgetId +
|
|
2429
|
+
"' has no writePaths granted for action '" +
|
|
2430
|
+
action +
|
|
2431
|
+
"'",
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
if (!_filenameMatches(filename, fsPerms.writePaths)) {
|
|
2435
|
+
return {
|
|
2436
|
+
allow: false,
|
|
2437
|
+
reason:
|
|
2438
|
+
"fs gate: filename '" +
|
|
2439
|
+
filename +
|
|
2440
|
+
"' rejected — not in allowed writePaths for widget '" +
|
|
2441
|
+
widgetId +
|
|
2442
|
+
"'",
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
return { allow: true };
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// Read action — may use readPaths OR writePaths (write implies read)
|
|
2449
|
+
if (
|
|
2450
|
+
_filenameMatches(filename, fsPerms.readPaths) ||
|
|
2451
|
+
_filenameMatches(filename, fsPerms.writePaths)
|
|
2452
|
+
) {
|
|
2453
|
+
return { allow: true };
|
|
2454
|
+
}
|
|
2455
|
+
return {
|
|
2456
|
+
allow: false,
|
|
2457
|
+
reason:
|
|
2458
|
+
"fs gate: filename '" +
|
|
2459
|
+
filename +
|
|
2460
|
+
"' rejected — not in allowed readPaths or writePaths for widget '" +
|
|
2461
|
+
widgetId +
|
|
2462
|
+
"'",
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* Merge an approved JIT decision's grant into the widget's existing
|
|
2468
|
+
* grant under domains.fs. Same shape as permissionGate._mergeGrant
|
|
2469
|
+
* but scoped to fs.
|
|
2470
|
+
*/
|
|
2471
|
+
function _mergeFsGrant(current, addition) {
|
|
2472
|
+
const out = {
|
|
2473
|
+
grantOrigin: addition.grantOrigin || current?.grantOrigin || null,
|
|
2474
|
+
servers: { ...(current?.servers || {}) },
|
|
2475
|
+
domains: { ...(current?.domains || {}) },
|
|
2476
|
+
};
|
|
2477
|
+
const additionFs = addition?.domains?.fs;
|
|
2478
|
+
if (additionFs) {
|
|
2479
|
+
const existingFs = out.domains.fs || { readPaths: [], writePaths: [] };
|
|
2480
|
+
out.domains.fs = {
|
|
2481
|
+
readPaths: [
|
|
2482
|
+
...new Set([
|
|
2483
|
+
...(existingFs.readPaths || []),
|
|
2484
|
+
...(Array.isArray(additionFs.readPaths) ? additionFs.readPaths : []),
|
|
2485
|
+
]),
|
|
2486
|
+
],
|
|
2487
|
+
writePaths: [
|
|
2488
|
+
...new Set([
|
|
2489
|
+
...(existingFs.writePaths || []),
|
|
2490
|
+
...(Array.isArray(additionFs.writePaths)
|
|
2491
|
+
? additionFs.writePaths
|
|
2492
|
+
: []),
|
|
2493
|
+
]),
|
|
2494
|
+
],
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
return out;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
/**
|
|
2501
|
+
* Async gate that escalates "no fs grant" denials to a JIT consent
|
|
2502
|
+
* prompt when `opts.enableJit` is true. On approval, merges the
|
|
2503
|
+
* decision's grant blob into the persisted grant and re-evaluates.
|
|
2504
|
+
*/
|
|
2505
|
+
async function gateFsCallWithJit$1(req, opts = {}) {
|
|
2506
|
+
const initial = gateFsCall$1(req);
|
|
2507
|
+
if (initial.allow) return initial;
|
|
2508
|
+
if (!opts.enableJit) return initial;
|
|
2509
|
+
if (!_isNoGrantDenial$1(initial.reason)) return initial;
|
|
2510
|
+
|
|
2511
|
+
let decision;
|
|
2512
|
+
try {
|
|
2513
|
+
decision = await requestApproval$1(
|
|
2514
|
+
{
|
|
2515
|
+
widgetId: req.widgetId,
|
|
2516
|
+
domain: "fs",
|
|
2517
|
+
action: req.action,
|
|
2518
|
+
args: req.args || {},
|
|
2519
|
+
},
|
|
2520
|
+
{ timeoutMs: opts.timeoutMs },
|
|
2521
|
+
);
|
|
2522
|
+
} catch (e) {
|
|
2523
|
+
return {
|
|
2524
|
+
allow: false,
|
|
2525
|
+
reason:
|
|
2526
|
+
"JIT consent " +
|
|
2527
|
+
(e && e.message ? e.message : "failed") +
|
|
2528
|
+
"; original denial: " +
|
|
2529
|
+
initial.reason,
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
if (!decision || decision.approve !== true) {
|
|
2534
|
+
return {
|
|
2535
|
+
allow: false,
|
|
2536
|
+
reason:
|
|
2537
|
+
"user declined JIT consent for widget '" +
|
|
2538
|
+
req.widgetId +
|
|
2539
|
+
"' calling fs '" +
|
|
2540
|
+
req.action +
|
|
2541
|
+
"'",
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
const filename = req.args?.filename || "*";
|
|
2546
|
+
const isWrite = isFsWriteAction(req.action);
|
|
2547
|
+
const addition =
|
|
2548
|
+
decision.granted && typeof decision.granted === "object"
|
|
2549
|
+
? decision.granted
|
|
2550
|
+
: {
|
|
2551
|
+
grantOrigin: "live",
|
|
2552
|
+
domains: {
|
|
2553
|
+
fs: {
|
|
2554
|
+
readPaths: !isWrite ? [filename] : [],
|
|
2555
|
+
writePaths: isWrite ? [filename] : [],
|
|
2556
|
+
},
|
|
2557
|
+
},
|
|
2558
|
+
};
|
|
2559
|
+
addition.grantOrigin = "live";
|
|
2560
|
+
|
|
2561
|
+
try {
|
|
2562
|
+
const current = getGrant$2(req.widgetId);
|
|
2563
|
+
const merged = _mergeFsGrant(current, addition);
|
|
2564
|
+
setGrant$2(req.widgetId, merged);
|
|
2565
|
+
} catch (e) {
|
|
2566
|
+
return {
|
|
2567
|
+
allow: false,
|
|
2568
|
+
reason:
|
|
2569
|
+
"JIT consent: failed to persist fs grant: " +
|
|
2570
|
+
(e && e.message ? e.message : String(e)),
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
return gateFsCall$1(req);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
var fsGate = {
|
|
2578
|
+
gateFsCall: gateFsCall$1,
|
|
2579
|
+
gateFsCallWithJit: gateFsCallWithJit$1,
|
|
2580
|
+
isFsWriteAction,
|
|
2581
|
+
WRITE_ACTIONS,
|
|
2582
|
+
};
|
|
2583
|
+
|
|
2584
|
+
/**
|
|
2585
|
+
* securityFlags.js
|
|
2586
|
+
*
|
|
2587
|
+
* Centralized readers for the two boolean security flags that gate the
|
|
2588
|
+
* MCP allowlist stack:
|
|
2589
|
+
* - security.enforceWidgetMcpPermissions
|
|
2590
|
+
* - security.enableJitConsent
|
|
2591
|
+
*
|
|
2592
|
+
* **Default semantics: ON.** A missing settings.json, a missing
|
|
2593
|
+
* `security` block, or an undefined field all yield `true`. Only an
|
|
2594
|
+
* explicit `false` opts out. This is intentional — the security stack
|
|
2595
|
+
* is on by default; users have to actively disable it. The
|
|
2596
|
+
* Privacy & Security panel surfaces the toggles + a confirm-on-disable
|
|
2597
|
+
* dialog so the disable path is deliberate.
|
|
2598
|
+
*
|
|
2599
|
+
* The readers are pure functions of a settings object so the
|
|
2600
|
+
* default-on semantics are pinned by unit tests without touching the
|
|
2601
|
+
* filesystem. The callers in mcpController.js wrap these with
|
|
2602
|
+
* settings.json IO.
|
|
2603
|
+
*/
|
|
2604
|
+
|
|
2605
|
+
function readEnforceFlag$2(settings) {
|
|
2606
|
+
return settings?.security?.enforceWidgetMcpPermissions !== false;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
function readJitFlag$2(settings) {
|
|
2610
|
+
return settings?.security?.enableJitConsent !== false;
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
var securityFlags = { readEnforceFlag: readEnforceFlag$2, readJitFlag: readJitFlag$2 };
|
|
2614
|
+
|
|
1826
2615
|
/**
|
|
1827
2616
|
* safeJsExecutor.js
|
|
1828
2617
|
*
|
|
@@ -1866,192 +2655,201 @@ var safePath_1 = {
|
|
|
1866
2655
|
* while QuickJS is executing synchronously.
|
|
1867
2656
|
*/
|
|
1868
2657
|
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2658
|
+
var safeJsExecutor;
|
|
2659
|
+
var hasRequiredSafeJsExecutor;
|
|
2660
|
+
|
|
2661
|
+
function requireSafeJsExecutor () {
|
|
2662
|
+
if (hasRequiredSafeJsExecutor) return safeJsExecutor;
|
|
2663
|
+
hasRequiredSafeJsExecutor = 1;
|
|
2664
|
+
|
|
2665
|
+
// quickjs-emscripten is loaded lazily inside getModule() rather than at
|
|
2666
|
+
// module top. Reason: when transform.js (which lives in the same dir)
|
|
2667
|
+
// does require("./safeJsExecutor") and rollup-plugin-commonjs sees a
|
|
2668
|
+
// statically-required external (`quickjs-emscripten`) inside that file,
|
|
2669
|
+
// it can mark the relative import as transitive-external and then fail
|
|
2670
|
+
// to resolve back. Deferring the require breaks the static-analysis
|
|
2671
|
+
// chain so the rollup build resolves cleanly.
|
|
2672
|
+
const DEFAULT_TIMEOUT_MS = 1000;
|
|
2673
|
+
const DEFAULT_MEMORY_BYTES = 32 * 1024 * 1024; // 32 MB
|
|
2674
|
+
|
|
2675
|
+
let _modulePromise = null;
|
|
2676
|
+
function getModule() {
|
|
2677
|
+
if (!_modulePromise) {
|
|
2678
|
+
const { getQuickJS } = require$$0$3;
|
|
2679
|
+
_modulePromise = getQuickJS();
|
|
2680
|
+
}
|
|
2681
|
+
return _modulePromise;
|
|
2682
|
+
}
|
|
1878
2683
|
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2684
|
+
function injectInputs(vm, args, inputs) {
|
|
2685
|
+
if (!Array.isArray(args) || !Array.isArray(inputs)) {
|
|
2686
|
+
throw new Error(
|
|
2687
|
+
"safeJsExecutor: args and inputs must be arrays of equal length",
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
if (args.length !== inputs.length) {
|
|
2691
|
+
throw new Error("safeJsExecutor: args.length must equal inputs.length");
|
|
2692
|
+
}
|
|
2693
|
+
for (let i = 0; i < args.length; i++) {
|
|
2694
|
+
const name = args[i];
|
|
2695
|
+
if (typeof name !== "string" || !/^[A-Za-z_$][\w$]*$/.test(name)) {
|
|
2696
|
+
throw new Error("safeJsExecutor: arg names must be valid JS identifiers");
|
|
2697
|
+
}
|
|
2698
|
+
const json = JSON.stringify(inputs[i]);
|
|
2699
|
+
// Use evalCode to materialize the JSON-typed value inside the
|
|
2700
|
+
// VM. For undefined inputs, JSON.stringify returns undefined
|
|
2701
|
+
// (not a string) — fall through to evaluating the literal
|
|
2702
|
+
// `undefined`.
|
|
2703
|
+
const literal = json === undefined ? "undefined" : json;
|
|
2704
|
+
const result = vm.evalCode(`(${literal})`);
|
|
2705
|
+
if (result.error) {
|
|
2706
|
+
const err = vm.dump(result.error);
|
|
2707
|
+
result.error.dispose();
|
|
2708
|
+
throw new Error(
|
|
2709
|
+
`safeJsExecutor: failed to inject "${name}": ${
|
|
2710
|
+
err && err.message ? err.message : err
|
|
2711
|
+
}`,
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
vm.setProp(vm.global, name, result.value);
|
|
2715
|
+
result.value.dispose();
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
1887
2718
|
|
|
1888
|
-
function
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
);
|
|
1893
|
-
}
|
|
1894
|
-
if (args.length !== inputs.length) {
|
|
1895
|
-
throw new Error("safeJsExecutor: args.length must equal inputs.length");
|
|
1896
|
-
}
|
|
1897
|
-
for (let i = 0; i < args.length; i++) {
|
|
1898
|
-
const name = args[i];
|
|
1899
|
-
if (typeof name !== "string" || !/^[A-Za-z_$][\w$]*$/.test(name)) {
|
|
1900
|
-
throw new Error("safeJsExecutor: arg names must be valid JS identifiers");
|
|
1901
|
-
}
|
|
1902
|
-
const json = JSON.stringify(inputs[i]);
|
|
1903
|
-
// Use evalCode to materialize the JSON-typed value inside the
|
|
1904
|
-
// VM. For undefined inputs, JSON.stringify returns undefined
|
|
1905
|
-
// (not a string) — fall through to evaluating the literal
|
|
1906
|
-
// `undefined`.
|
|
1907
|
-
const literal = json === undefined ? "undefined" : json;
|
|
1908
|
-
const result = vm.evalCode(`(${literal})`);
|
|
1909
|
-
if (result.error) {
|
|
1910
|
-
const err = vm.dump(result.error);
|
|
1911
|
-
result.error.dispose();
|
|
1912
|
-
throw new Error(
|
|
1913
|
-
`safeJsExecutor: failed to inject "${name}": ${
|
|
1914
|
-
err && err.message ? err.message : err
|
|
1915
|
-
}`,
|
|
1916
|
-
);
|
|
1917
|
-
}
|
|
1918
|
-
vm.setProp(vm.global, name, result.value);
|
|
1919
|
-
result.value.dispose();
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
2719
|
+
function setDeadline(vm, timeoutMs) {
|
|
2720
|
+
const deadline = Date.now() + Math.max(1, timeoutMs);
|
|
2721
|
+
vm.runtime.setInterruptHandler(() => Date.now() > deadline);
|
|
2722
|
+
}
|
|
1922
2723
|
|
|
1923
|
-
function
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2724
|
+
function buildWrappedBody(args, body) {
|
|
2725
|
+
// Wrap the user body in an IIFE so `return` works at the body level
|
|
2726
|
+
// (matching the dynamic-function-constructor semantics this is
|
|
2727
|
+
// replacing).
|
|
2728
|
+
return `(function(${args.join(",")}){${body}})(${args.join(",")})`;
|
|
2729
|
+
}
|
|
1927
2730
|
|
|
1928
|
-
function
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2731
|
+
async function runOnce({
|
|
2732
|
+
body,
|
|
2733
|
+
args = [],
|
|
2734
|
+
inputs = [],
|
|
2735
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
2736
|
+
memoryBytes = DEFAULT_MEMORY_BYTES,
|
|
2737
|
+
}) {
|
|
2738
|
+
if (typeof body !== "string" || !body.trim()) {
|
|
2739
|
+
return { error: "body must be a non-empty string" };
|
|
2740
|
+
}
|
|
2741
|
+
const QuickJS = await getModule();
|
|
2742
|
+
const vm = QuickJS.newContext();
|
|
2743
|
+
try {
|
|
2744
|
+
vm.runtime.setMemoryLimit(memoryBytes);
|
|
2745
|
+
vm.runtime.setMaxStackSize(1024 * 1024);
|
|
2746
|
+
setDeadline(vm, timeoutMs);
|
|
1934
2747
|
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
inputs = [],
|
|
1939
|
-
timeoutMs = DEFAULT_TIMEOUT_MS$1,
|
|
1940
|
-
memoryBytes = DEFAULT_MEMORY_BYTES,
|
|
1941
|
-
}) {
|
|
1942
|
-
if (typeof body !== "string" || !body.trim()) {
|
|
1943
|
-
return { error: "body must be a non-empty string" };
|
|
1944
|
-
}
|
|
1945
|
-
const QuickJS = await getModule();
|
|
1946
|
-
const vm = QuickJS.newContext();
|
|
1947
|
-
try {
|
|
1948
|
-
vm.runtime.setMemoryLimit(memoryBytes);
|
|
1949
|
-
vm.runtime.setMaxStackSize(1024 * 1024);
|
|
1950
|
-
setDeadline(vm, timeoutMs);
|
|
2748
|
+
injectInputs(vm, args, inputs);
|
|
2749
|
+
const wrapped = buildWrappedBody(args, body);
|
|
2750
|
+
const result = vm.evalCode(wrapped);
|
|
1951
2751
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2752
|
+
if (result.error) {
|
|
2753
|
+
const err = vm.dump(result.error);
|
|
2754
|
+
result.error.dispose();
|
|
2755
|
+
return {
|
|
2756
|
+
error:
|
|
2757
|
+
err && err.message
|
|
2758
|
+
? String(err.message)
|
|
2759
|
+
: typeof err === "string"
|
|
2760
|
+
? err
|
|
2761
|
+
: JSON.stringify(err),
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
const value = vm.dump(result.value);
|
|
2765
|
+
result.value.dispose();
|
|
2766
|
+
return { value };
|
|
2767
|
+
} catch (e) {
|
|
2768
|
+
return { error: e && e.message ? e.message : String(e) };
|
|
2769
|
+
} finally {
|
|
2770
|
+
vm.dispose();
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
1955
2773
|
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2774
|
+
async function createCompiled({
|
|
2775
|
+
body,
|
|
2776
|
+
args = [],
|
|
2777
|
+
memoryBytes = DEFAULT_MEMORY_BYTES,
|
|
2778
|
+
}) {
|
|
2779
|
+
if (typeof body !== "string" || !body.trim()) {
|
|
2780
|
+
throw new Error("body must be a non-empty string");
|
|
2781
|
+
}
|
|
2782
|
+
const QuickJS = await getModule();
|
|
2783
|
+
const vm = QuickJS.newContext();
|
|
2784
|
+
vm.runtime.setMemoryLimit(memoryBytes);
|
|
2785
|
+
vm.runtime.setMaxStackSize(1024 * 1024);
|
|
2786
|
+
|
|
2787
|
+
// Define the user function once on the VM globals so subsequent
|
|
2788
|
+
// run() calls can invoke it with fresh args without re-parsing.
|
|
2789
|
+
const define = vm.evalCode(
|
|
2790
|
+
`globalThis.__userFn = function(${args.join(",")}){${body}};`,
|
|
2791
|
+
);
|
|
2792
|
+
if (define.error) {
|
|
2793
|
+
const err = vm.dump(define.error);
|
|
2794
|
+
define.error.dispose();
|
|
2795
|
+
vm.dispose();
|
|
2796
|
+
throw new Error(
|
|
2797
|
+
"safeJsExecutor: compile failed: " +
|
|
2798
|
+
(err && err.message ? err.message : JSON.stringify(err)),
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
define.value.dispose();
|
|
1977
2802
|
|
|
1978
|
-
|
|
1979
|
-
body,
|
|
1980
|
-
args = [],
|
|
1981
|
-
memoryBytes = DEFAULT_MEMORY_BYTES,
|
|
1982
|
-
}) {
|
|
1983
|
-
if (typeof body !== "string" || !body.trim()) {
|
|
1984
|
-
throw new Error("body must be a non-empty string");
|
|
1985
|
-
}
|
|
1986
|
-
const QuickJS = await getModule();
|
|
1987
|
-
const vm = QuickJS.newContext();
|
|
1988
|
-
vm.runtime.setMemoryLimit(memoryBytes);
|
|
1989
|
-
vm.runtime.setMaxStackSize(1024 * 1024);
|
|
1990
|
-
|
|
1991
|
-
// Define the user function once on the VM globals so subsequent
|
|
1992
|
-
// run() calls can invoke it with fresh args without re-parsing.
|
|
1993
|
-
const define = vm.evalCode(
|
|
1994
|
-
`globalThis.__userFn = function(${args.join(",")}){${body}};`,
|
|
1995
|
-
);
|
|
1996
|
-
if (define.error) {
|
|
1997
|
-
const err = vm.dump(define.error);
|
|
1998
|
-
define.error.dispose();
|
|
1999
|
-
vm.dispose();
|
|
2000
|
-
throw new Error(
|
|
2001
|
-
"safeJsExecutor: compile failed: " +
|
|
2002
|
-
(err && err.message ? err.message : JSON.stringify(err)),
|
|
2003
|
-
);
|
|
2004
|
-
}
|
|
2005
|
-
define.value.dispose();
|
|
2803
|
+
let disposed = false;
|
|
2006
2804
|
|
|
2007
|
-
|
|
2805
|
+
return {
|
|
2806
|
+
run(inputs, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2807
|
+
if (disposed) {
|
|
2808
|
+
return { error: "executor already disposed" };
|
|
2809
|
+
}
|
|
2810
|
+
try {
|
|
2811
|
+
setDeadline(vm, timeoutMs);
|
|
2812
|
+
const argList = inputs
|
|
2813
|
+
.map((v) => (v === undefined ? "undefined" : JSON.stringify(v)))
|
|
2814
|
+
.join(",");
|
|
2815
|
+
const result = vm.evalCode(`__userFn(${argList})`);
|
|
2816
|
+
if (result.error) {
|
|
2817
|
+
const err = vm.dump(result.error);
|
|
2818
|
+
result.error.dispose();
|
|
2819
|
+
return {
|
|
2820
|
+
error:
|
|
2821
|
+
err && err.message
|
|
2822
|
+
? String(err.message)
|
|
2823
|
+
: typeof err === "string"
|
|
2824
|
+
? err
|
|
2825
|
+
: JSON.stringify(err),
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
const value = vm.dump(result.value);
|
|
2829
|
+
result.value.dispose();
|
|
2830
|
+
return { value };
|
|
2831
|
+
} catch (e) {
|
|
2832
|
+
return { error: e && e.message ? e.message : String(e) };
|
|
2833
|
+
}
|
|
2834
|
+
},
|
|
2835
|
+
dispose() {
|
|
2836
|
+
if (!disposed) {
|
|
2837
|
+
disposed = true;
|
|
2838
|
+
vm.dispose();
|
|
2839
|
+
}
|
|
2840
|
+
},
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2008
2843
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
const argList = inputs
|
|
2017
|
-
.map((v) => (v === undefined ? "undefined" : JSON.stringify(v)))
|
|
2018
|
-
.join(",");
|
|
2019
|
-
const result = vm.evalCode(`__userFn(${argList})`);
|
|
2020
|
-
if (result.error) {
|
|
2021
|
-
const err = vm.dump(result.error);
|
|
2022
|
-
result.error.dispose();
|
|
2023
|
-
return {
|
|
2024
|
-
error:
|
|
2025
|
-
err && err.message
|
|
2026
|
-
? String(err.message)
|
|
2027
|
-
: typeof err === "string"
|
|
2028
|
-
? err
|
|
2029
|
-
: JSON.stringify(err),
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
const value = vm.dump(result.value);
|
|
2033
|
-
result.value.dispose();
|
|
2034
|
-
return { value };
|
|
2035
|
-
} catch (e) {
|
|
2036
|
-
return { error: e && e.message ? e.message : String(e) };
|
|
2037
|
-
}
|
|
2038
|
-
},
|
|
2039
|
-
dispose() {
|
|
2040
|
-
if (!disposed) {
|
|
2041
|
-
disposed = true;
|
|
2042
|
-
vm.dispose();
|
|
2043
|
-
}
|
|
2044
|
-
},
|
|
2045
|
-
};
|
|
2844
|
+
safeJsExecutor = {
|
|
2845
|
+
runOnce,
|
|
2846
|
+
createCompiled,
|
|
2847
|
+
DEFAULT_TIMEOUT_MS,
|
|
2848
|
+
DEFAULT_MEMORY_BYTES,
|
|
2849
|
+
};
|
|
2850
|
+
return safeJsExecutor;
|
|
2046
2851
|
}
|
|
2047
2852
|
|
|
2048
|
-
var safeJsExecutor$1 = {
|
|
2049
|
-
runOnce,
|
|
2050
|
-
createCompiled,
|
|
2051
|
-
DEFAULT_TIMEOUT_MS: DEFAULT_TIMEOUT_MS$1,
|
|
2052
|
-
DEFAULT_MEMORY_BYTES,
|
|
2053
|
-
};
|
|
2054
|
-
|
|
2055
2853
|
/**
|
|
2056
2854
|
* Utils/tranaform
|
|
2057
2855
|
* Main gial is to transform a file of data into another form (CSV -> Json for example)
|
|
@@ -2060,462 +2858,514 @@ var safeJsExecutor$1 = {
|
|
|
2060
2858
|
* - CSV -> JSON
|
|
2061
2859
|
*/
|
|
2062
2860
|
|
|
2063
|
-
var
|
|
2064
|
-
var
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
var
|
|
2070
|
-
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
const
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
_transform(value, encoding, callback) {
|
|
2089
|
-
this.push(JSON.parse(value));
|
|
2090
|
-
callback();
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
let Transform$1 = class Transform {
|
|
2095
|
-
constructor() {
|
|
2096
|
-
console.log("constructor");
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
/**
|
|
2100
|
-
* readLinesFromFile
|
|
2101
|
-
*
|
|
2102
|
-
* If this is a json file we should check in and remove the newLines, and then
|
|
2103
|
-
* add commas between records potentially...
|
|
2104
|
-
* @param {*} win
|
|
2105
|
-
* @param {*} filepath
|
|
2106
|
-
* @param {*} linesCount
|
|
2107
|
-
* @param {*} callbackEvent
|
|
2108
|
-
* @returns
|
|
2109
|
-
*/
|
|
2110
|
-
readLinesFromFile = (
|
|
2111
|
-
win,
|
|
2112
|
-
filepath,
|
|
2113
|
-
linesCount = 100,
|
|
2114
|
-
callbackEvent = null,
|
|
2115
|
-
) => {
|
|
2116
|
-
return new Promise((resolve, reject) => {
|
|
2117
|
-
try {
|
|
2118
|
-
let count = 0;
|
|
2119
|
-
let lines = [];
|
|
2861
|
+
var transform;
|
|
2862
|
+
var hasRequiredTransform;
|
|
2863
|
+
|
|
2864
|
+
function requireTransform () {
|
|
2865
|
+
if (hasRequiredTransform) return transform;
|
|
2866
|
+
hasRequiredTransform = 1;
|
|
2867
|
+
var fs = require$$0$2;
|
|
2868
|
+
var readline = require$$1$3;
|
|
2869
|
+
const xtreamer = require$$2;
|
|
2870
|
+
var xmlParser = require$$3$1;
|
|
2871
|
+
var JSONStream = require$$4;
|
|
2872
|
+
const stream = require$$5;
|
|
2873
|
+
var csv = require$$6;
|
|
2874
|
+
const path = require$$1$2;
|
|
2875
|
+
const { app } = require$$0$1;
|
|
2876
|
+
const { ensureDirectoryExistence } = file;
|
|
2877
|
+
const safeJsExecutor = requireSafeJsExecutor();
|
|
2878
|
+
|
|
2879
|
+
const TRANSFORM_APP_NAME = "Dashboard";
|
|
2880
|
+
const MAX_MAPPING_BODY_SIZE = 10240; // 10KB limit for mapping function body
|
|
2881
|
+
// Per-record execution limit. Each mapping function call inside the
|
|
2882
|
+
// streaming transform must complete within this budget; a runaway loop
|
|
2883
|
+
// in the user's mapping triggers an "interrupted" error and the record
|
|
2884
|
+
// is skipped rather than blocking the whole stream.
|
|
2885
|
+
const PER_RECORD_TIMEOUT_MS = 1000;
|
|
2120
2886
|
|
|
2121
|
-
|
|
2122
|
-
|
|
2887
|
+
/**
|
|
2888
|
+
* XtreamerClientTransform
|
|
2889
|
+
* Custom Transform stream to parse the JSON from the XML to String operation
|
|
2890
|
+
*/
|
|
2891
|
+
class XtreamerClientTransform extends stream.Transform {
|
|
2892
|
+
_transform(value, encoding, callback) {
|
|
2893
|
+
this.push(JSON.parse(value));
|
|
2894
|
+
callback();
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2123
2897
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
});
|
|
2898
|
+
class Transform {
|
|
2899
|
+
constructor() {
|
|
2900
|
+
console.log("constructor");
|
|
2901
|
+
}
|
|
2129
2902
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2903
|
+
/**
|
|
2904
|
+
* readLinesFromFile
|
|
2905
|
+
*
|
|
2906
|
+
* If this is a json file we should check in and remove the newLines, and then
|
|
2907
|
+
* add commas between records potentially...
|
|
2908
|
+
* @param {*} win
|
|
2909
|
+
* @param {*} filepath
|
|
2910
|
+
* @param {*} linesCount
|
|
2911
|
+
* @param {*} callbackEvent
|
|
2912
|
+
* @returns
|
|
2913
|
+
*/
|
|
2914
|
+
readLinesFromFile = (
|
|
2915
|
+
win,
|
|
2916
|
+
filepath,
|
|
2917
|
+
linesCount = 100,
|
|
2918
|
+
callbackEvent = null,
|
|
2919
|
+
) => {
|
|
2920
|
+
return new Promise((resolve, reject) => {
|
|
2921
|
+
try {
|
|
2922
|
+
let count = 0;
|
|
2923
|
+
let lines = [];
|
|
2150
2924
|
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
file = "",
|
|
2154
|
-
objectCount = 10,
|
|
2155
|
-
callbackEvent = null,
|
|
2156
|
-
) => {
|
|
2157
|
-
return new Promise((resolve, reject) => {
|
|
2158
|
-
try {
|
|
2159
|
-
const parser = JSONStream$1.parse("*");
|
|
2160
|
-
const readStream = fs$e.createReadStream(file).pipe(parser);
|
|
2925
|
+
// can we aggregate potentially?
|
|
2926
|
+
let lineObject = [];
|
|
2161
2927
|
|
|
2162
|
-
|
|
2928
|
+
const readInterface = readline.createInterface({
|
|
2929
|
+
input: fs.createReadStream(filepath),
|
|
2930
|
+
output: process.stdout,
|
|
2931
|
+
console: false,
|
|
2932
|
+
});
|
|
2163
2933
|
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2934
|
+
readInterface.on("line", function (line) {
|
|
2935
|
+
if (count < linesCount) {
|
|
2936
|
+
//console.log(line);
|
|
2937
|
+
lines.push(line);
|
|
2938
|
+
count++;
|
|
2939
|
+
if (callbackEvent !== null) {
|
|
2940
|
+
win.webContents.send(callbackEvent, {
|
|
2941
|
+
line,
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
} else {
|
|
2945
|
+
readInterface.close();
|
|
2946
|
+
resolve(lines);
|
|
2947
|
+
}
|
|
2948
|
+
});
|
|
2949
|
+
} catch (e) {
|
|
2950
|
+
reject(e);
|
|
2951
|
+
}
|
|
2952
|
+
});
|
|
2953
|
+
};
|
|
2177
2954
|
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2955
|
+
readJSONFromFile = (
|
|
2956
|
+
win,
|
|
2957
|
+
file = "",
|
|
2958
|
+
objectCount = 10,
|
|
2959
|
+
callbackEvent = null,
|
|
2960
|
+
) => {
|
|
2961
|
+
return new Promise((resolve, reject) => {
|
|
2962
|
+
try {
|
|
2963
|
+
const parser = JSONStream.parse("*");
|
|
2964
|
+
const readStream = fs.createReadStream(file).pipe(parser);
|
|
2181
2965
|
|
|
2182
|
-
|
|
2183
|
-
resolve("Complete");
|
|
2184
|
-
});
|
|
2185
|
-
} catch (e) {
|
|
2186
|
-
console.log("read json error ", e);
|
|
2187
|
-
reject(e);
|
|
2188
|
-
}
|
|
2189
|
-
});
|
|
2190
|
-
};
|
|
2966
|
+
let count = 0;
|
|
2191
2967
|
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
})
|
|
2206
|
-
.catch((e) => reject(e));
|
|
2207
|
-
});
|
|
2208
|
-
}
|
|
2968
|
+
readStream.on("data", (data) => {
|
|
2969
|
+
//console.log(data);
|
|
2970
|
+
if (callbackEvent !== null) {
|
|
2971
|
+
win.webContents.send(callbackEvent, {
|
|
2972
|
+
data: JSON.stringify(data),
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
if (objectCount !== null && count < objectCount) {
|
|
2976
|
+
readStream.destroy();
|
|
2977
|
+
resolve("complete");
|
|
2978
|
+
}
|
|
2979
|
+
count++;
|
|
2980
|
+
});
|
|
2209
2981
|
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const xmlFileReadStream = fs$e.createReadStream(filepath);
|
|
2982
|
+
readStream.on("error", (e) => {
|
|
2983
|
+
reject(e);
|
|
2984
|
+
});
|
|
2214
2985
|
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2986
|
+
readStream.on("end", (data) => {
|
|
2987
|
+
resolve("Complete");
|
|
2988
|
+
});
|
|
2989
|
+
} catch (e) {
|
|
2990
|
+
console.log("read json error ", e);
|
|
2991
|
+
reject(e);
|
|
2992
|
+
}
|
|
2993
|
+
});
|
|
2994
|
+
};
|
|
2219
2995
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2996
|
+
async parseXMLString(data) {
|
|
2997
|
+
let xmlText = data.toString().replace("\ufeff", "");
|
|
2998
|
+
return new Promise((resolve, reject) => {
|
|
2999
|
+
xmlParser
|
|
3000
|
+
.parseStringPromise(xmlText, {
|
|
3001
|
+
trim: true,
|
|
3002
|
+
compact: true,
|
|
3003
|
+
ignoreComment: true,
|
|
3004
|
+
ignoreDoctype: true,
|
|
3005
|
+
})
|
|
3006
|
+
.then((data) => {
|
|
3007
|
+
// resolve with the comma
|
|
3008
|
+
resolve(JSON.stringify(data) + ",");
|
|
3009
|
+
})
|
|
3010
|
+
.catch((e) => reject(e));
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
2223
3013
|
|
|
2224
|
-
|
|
2225
|
-
|
|
3014
|
+
parseXMLStream = (filepath, outpath, start) => {
|
|
3015
|
+
return new Promise((resolve, reject) => {
|
|
3016
|
+
try {
|
|
3017
|
+
const xmlFileReadStream = fs.createReadStream(filepath);
|
|
2226
3018
|
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
simple: false,
|
|
2232
|
-
max_xml_size: 50000000, // 10000000
|
|
2233
|
-
};
|
|
3019
|
+
xmlFileReadStream.on("end", () => {
|
|
3020
|
+
writeStream.write("\n]");
|
|
3021
|
+
resolve("Read End");
|
|
3022
|
+
});
|
|
2234
3023
|
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
transformer: this.parseXMLString, // returns Promise
|
|
2239
|
-
max_xml_size: 50000000,
|
|
2240
|
-
},
|
|
2241
|
-
options,
|
|
2242
|
-
).on("error", (e) => {
|
|
2243
|
-
console.log(e);
|
|
2244
|
-
reject(e);
|
|
2245
|
-
});
|
|
3024
|
+
xmlFileReadStream.on("finished", () => {
|
|
3025
|
+
resolve("Read Finish");
|
|
3026
|
+
});
|
|
2246
3027
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
.pipe(new XtreamerClientTransform())
|
|
2250
|
-
.pipe(writeStream)
|
|
2251
|
-
.on("end", () => {
|
|
2252
|
-
console.log("ended");
|
|
2253
|
-
})
|
|
2254
|
-
.on("finish", () => {
|
|
2255
|
-
console.log("finished pipe");
|
|
2256
|
-
});
|
|
2257
|
-
} catch (e) {
|
|
2258
|
-
console.log(e);
|
|
2259
|
-
reject(e);
|
|
2260
|
-
}
|
|
2261
|
-
});
|
|
2262
|
-
};
|
|
3028
|
+
const writeStream = fs.createWriteStream(outpath);
|
|
3029
|
+
writeStream.write("[\n");
|
|
2263
3030
|
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
callbackEvent = null,
|
|
2272
|
-
limit = null,
|
|
2273
|
-
) => {
|
|
2274
|
-
return new Promise((resolve, reject) => {
|
|
2275
|
-
try {
|
|
2276
|
-
const readStream = fs$e
|
|
2277
|
-
.createReadStream(filepath)
|
|
2278
|
-
.pipe(csv({ separator: delimiter }));
|
|
2279
|
-
const writeStream = fs$e.createWriteStream(outpath);
|
|
2280
|
-
|
|
2281
|
-
let canParse = true;
|
|
3031
|
+
const options = {
|
|
3032
|
+
headers: { Accept: "application/xml" },
|
|
3033
|
+
resolveWithFullResponse: true,
|
|
3034
|
+
json: false,
|
|
3035
|
+
simple: false,
|
|
3036
|
+
max_xml_size: 50000000, // 10000000
|
|
3037
|
+
};
|
|
2282
3038
|
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
3039
|
+
const xtreamerTransform = xtreamer(
|
|
3040
|
+
start,
|
|
3041
|
+
{
|
|
3042
|
+
transformer: this.parseXMLString, // returns Promise
|
|
3043
|
+
max_xml_size: 50000000,
|
|
3044
|
+
},
|
|
3045
|
+
options,
|
|
3046
|
+
).on("error", (e) => {
|
|
3047
|
+
console.log(e);
|
|
3048
|
+
reject(e);
|
|
3049
|
+
});
|
|
2287
3050
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
3051
|
+
xmlFileReadStream
|
|
3052
|
+
.pipe(xtreamerTransform)
|
|
3053
|
+
.pipe(new XtreamerClientTransform())
|
|
3054
|
+
.pipe(writeStream)
|
|
3055
|
+
.on("end", () => {
|
|
3056
|
+
console.log("ended");
|
|
3057
|
+
})
|
|
3058
|
+
.on("finish", () => {
|
|
3059
|
+
console.log("finished pipe");
|
|
3060
|
+
});
|
|
3061
|
+
} catch (e) {
|
|
3062
|
+
console.log(e);
|
|
3063
|
+
reject(e);
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
};
|
|
2303
3067
|
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
3068
|
+
parseCSVStream = (
|
|
3069
|
+
filepath,
|
|
3070
|
+
outpath,
|
|
3071
|
+
delimiter = ",",
|
|
3072
|
+
objectIdKey = null,
|
|
3073
|
+
headers = null, // optional array of headings to grab
|
|
3074
|
+
win = null,
|
|
3075
|
+
callbackEvent = null,
|
|
3076
|
+
limit = null,
|
|
3077
|
+
) => {
|
|
3078
|
+
return new Promise((resolve, reject) => {
|
|
3079
|
+
try {
|
|
3080
|
+
const readStream = fs
|
|
3081
|
+
.createReadStream(filepath)
|
|
3082
|
+
.pipe(csv({ separator: delimiter }));
|
|
3083
|
+
const writeStream = fs.createWriteStream(outpath);
|
|
2312
3084
|
|
|
2313
|
-
|
|
2314
|
-
writeStream.write("]");
|
|
2315
|
-
readStream.destroy();
|
|
2316
|
-
resolve("Complete");
|
|
2317
|
-
});
|
|
2318
|
-
} catch (e) {
|
|
2319
|
-
reject(e);
|
|
2320
|
-
}
|
|
2321
|
-
});
|
|
2322
|
-
};
|
|
3085
|
+
let canParse = true;
|
|
2323
3086
|
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
*
|
|
2329
|
-
* @param {*} win
|
|
2330
|
-
* @param {*} filepath
|
|
2331
|
-
* @param {*} outFilepath
|
|
2332
|
-
* @param {*} mapping
|
|
2333
|
-
*/
|
|
2334
|
-
transformFileToFile = (
|
|
2335
|
-
win,
|
|
2336
|
-
filepath,
|
|
2337
|
-
outFilepath,
|
|
2338
|
-
mappingFunctionBody,
|
|
2339
|
-
args = ["refObj", "index"],
|
|
2340
|
-
callbackEvent = null,
|
|
2341
|
-
) => {
|
|
2342
|
-
return new Promise((resolve, reject) => {
|
|
2343
|
-
// Validate mappingFunctionBody is a non-empty string
|
|
2344
|
-
if (
|
|
2345
|
-
typeof mappingFunctionBody !== "string" ||
|
|
2346
|
-
!mappingFunctionBody.trim()
|
|
2347
|
-
) {
|
|
2348
|
-
return reject(
|
|
2349
|
-
new Error("mappingFunctionBody must be a non-empty string"),
|
|
2350
|
-
);
|
|
2351
|
-
}
|
|
3087
|
+
// separators for JSON
|
|
3088
|
+
let sep = "";
|
|
3089
|
+
let count = 0;
|
|
3090
|
+
writeStream.write("[\n");
|
|
2352
3091
|
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
3092
|
+
readStream.on("data", (item) => {
|
|
3093
|
+
if (count > 0) {
|
|
3094
|
+
sep = ",\n";
|
|
3095
|
+
}
|
|
3096
|
+
// if we have specified a limit...
|
|
3097
|
+
if (limit !== null && limit <= count) {
|
|
3098
|
+
canParse = false;
|
|
3099
|
+
writeStream.write("]");
|
|
3100
|
+
readStream.destroy();
|
|
3101
|
+
resolve("Complete");
|
|
3102
|
+
}
|
|
3103
|
+
if (canParse === true) {
|
|
3104
|
+
item["objectID"] = item[objectIdKey];
|
|
3105
|
+
writeStream.write(sep + JSON.stringify(item));
|
|
3106
|
+
count++;
|
|
3107
|
+
|
|
3108
|
+
if (win !== null) {
|
|
3109
|
+
//console.log("have win", count, callbackEvent);
|
|
3110
|
+
win.webContents.send(callbackEvent, {
|
|
3111
|
+
count,
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
2363
3116
|
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
3117
|
+
readStream.on("end", () => {
|
|
3118
|
+
writeStream.write("]");
|
|
3119
|
+
readStream.destroy();
|
|
3120
|
+
resolve("Complete");
|
|
3121
|
+
});
|
|
3122
|
+
} catch (e) {
|
|
3123
|
+
reject(e);
|
|
3124
|
+
}
|
|
3125
|
+
});
|
|
3126
|
+
};
|
|
2368
3127
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
3128
|
+
/**
|
|
3129
|
+
* transformFileToFile
|
|
3130
|
+
* We want to convert a format from a file into a different format
|
|
3131
|
+
* based on the mapper we have provided
|
|
3132
|
+
*
|
|
3133
|
+
* @param {*} win
|
|
3134
|
+
* @param {*} filepath
|
|
3135
|
+
* @param {*} outFilepath
|
|
3136
|
+
* @param {*} mapping
|
|
3137
|
+
*/
|
|
3138
|
+
transformFileToFile = (
|
|
3139
|
+
win,
|
|
3140
|
+
filepath,
|
|
3141
|
+
outFilepath,
|
|
3142
|
+
mappingFunctionBody,
|
|
3143
|
+
args = ["refObj", "index"],
|
|
3144
|
+
callbackEvent = null,
|
|
3145
|
+
) => {
|
|
3146
|
+
return new Promise((resolve, reject) => {
|
|
3147
|
+
// Validate mappingFunctionBody is a non-empty string
|
|
3148
|
+
if (
|
|
3149
|
+
typeof mappingFunctionBody !== "string" ||
|
|
3150
|
+
!mappingFunctionBody.trim()
|
|
3151
|
+
) {
|
|
3152
|
+
return reject(
|
|
3153
|
+
new Error("mappingFunctionBody must be a non-empty string"),
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
2383
3156
|
|
|
2384
|
-
|
|
2385
|
-
|
|
3157
|
+
// Enforce size limit on mapping function body
|
|
3158
|
+
if (mappingFunctionBody.length > MAX_MAPPING_BODY_SIZE) {
|
|
3159
|
+
return reject(
|
|
3160
|
+
new Error(
|
|
3161
|
+
"mappingFunctionBody exceeds maximum size of " +
|
|
3162
|
+
MAX_MAPPING_BODY_SIZE +
|
|
3163
|
+
" bytes",
|
|
3164
|
+
),
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
2386
3167
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
// create the readStream to parse the large file (json)
|
|
2392
|
-
var readStream = fs$e.createReadStream(resolvedFilepath).pipe(parser);
|
|
3168
|
+
// Validate file paths are within app data directory
|
|
3169
|
+
const appDataDir = path.join(app.getPath("userData"), TRANSFORM_APP_NAME);
|
|
3170
|
+
const resolvedFilepath = path.resolve(filepath);
|
|
3171
|
+
const resolvedOutFilepath = path.resolve(outFilepath);
|
|
2393
3172
|
|
|
2394
|
-
|
|
3173
|
+
if (!resolvedFilepath.startsWith(appDataDir + path.sep)) {
|
|
3174
|
+
return reject(
|
|
3175
|
+
new Error(
|
|
3176
|
+
"Input file path must be within the application data directory",
|
|
3177
|
+
),
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
if (!resolvedOutFilepath.startsWith(appDataDir + path.sep)) {
|
|
3181
|
+
return reject(
|
|
3182
|
+
new Error(
|
|
3183
|
+
"Output file path must be within the application data directory",
|
|
3184
|
+
),
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
2395
3187
|
|
|
2396
|
-
|
|
3188
|
+
// JSON parser
|
|
3189
|
+
var parser = JSONStream.parse("*");
|
|
2397
3190
|
|
|
2398
|
-
|
|
2399
|
-
|
|
3191
|
+
if (!fs.existsSync(resolvedFilepath)) {
|
|
3192
|
+
return reject(new Error("File doesnt exist"));
|
|
3193
|
+
}
|
|
3194
|
+
console.log("file exists ", resolvedFilepath);
|
|
3195
|
+
// create the readStream to parse the large file (json)
|
|
3196
|
+
var readStream = fs.createReadStream(resolvedFilepath).pipe(parser);
|
|
3197
|
+
|
|
3198
|
+
ensureDirectoryExistence(resolvedOutFilepath);
|
|
3199
|
+
|
|
3200
|
+
var writeStream = fs.createWriteStream(resolvedOutFilepath);
|
|
3201
|
+
|
|
3202
|
+
let sep = "";
|
|
3203
|
+
let count = 0;
|
|
3204
|
+
|
|
3205
|
+
// Compile the user-supplied mapping function inside a QuickJS
|
|
3206
|
+
// sandbox. The previous implementation compiled the body with
|
|
3207
|
+
// full Node.js privileges (filesystem, network, process). The
|
|
3208
|
+
// sandbox gives the body a tiny pure-JS surface (Math, JSON,
|
|
3209
|
+
// Date, primitives) and no host globals — see
|
|
3210
|
+
// electron/utils/safeJsExecutor.js for full rationale.
|
|
3211
|
+
//
|
|
3212
|
+
// createCompiled is async (the WASM module must be loaded). Use
|
|
3213
|
+
// .then/.catch instead of await because we're inside a sync
|
|
3214
|
+
// Promise executor.
|
|
3215
|
+
safeJsExecutor
|
|
3216
|
+
.createCompiled({
|
|
3217
|
+
body: mappingFunctionBody,
|
|
3218
|
+
args,
|
|
3219
|
+
})
|
|
3220
|
+
.then((executor) => {
|
|
3221
|
+
startStreamingWithExecutor(executor);
|
|
3222
|
+
})
|
|
3223
|
+
.catch((e) => {
|
|
3224
|
+
reject(
|
|
3225
|
+
new Error(
|
|
3226
|
+
"mappingFunctionBody failed to compile: " +
|
|
3227
|
+
(e && e.message ? e.message : String(e)),
|
|
3228
|
+
),
|
|
3229
|
+
);
|
|
3230
|
+
});
|
|
2400
3231
|
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
// sandbox gives the body a tiny pure-JS surface (Math, JSON,
|
|
2405
|
-
// Date, primitives) and no host globals — see
|
|
2406
|
-
// electron/utils/safeJsExecutor.js for full rationale.
|
|
2407
|
-
//
|
|
2408
|
-
// createCompiled is async (the WASM module must be loaded). Use
|
|
2409
|
-
// .then/.catch instead of await because we're inside a sync
|
|
2410
|
-
// Promise executor.
|
|
2411
|
-
safeJsExecutor
|
|
2412
|
-
.createCompiled({
|
|
2413
|
-
body: mappingFunctionBody,
|
|
2414
|
-
args,
|
|
2415
|
-
})
|
|
2416
|
-
.then((executor) => {
|
|
2417
|
-
startStreamingWithExecutor(executor);
|
|
2418
|
-
})
|
|
2419
|
-
.catch((e) => {
|
|
2420
|
-
reject(
|
|
2421
|
-
new Error(
|
|
2422
|
-
"mappingFunctionBody failed to compile: " +
|
|
2423
|
-
(e && e.message ? e.message : String(e)),
|
|
2424
|
-
),
|
|
2425
|
-
);
|
|
2426
|
-
});
|
|
3232
|
+
function startStreamingWithExecutor(executor) {
|
|
3233
|
+
// begin the write stream
|
|
3234
|
+
writeStream.write("[\n");
|
|
2427
3235
|
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
3236
|
+
readStream.on("data", (data) => {
|
|
3237
|
+
try {
|
|
3238
|
+
if (count > 0) {
|
|
3239
|
+
sep = ",\n";
|
|
3240
|
+
}
|
|
2431
3241
|
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
count +
|
|
2447
|
-
": " +
|
|
2448
|
-
result.error,
|
|
2449
|
-
);
|
|
2450
|
-
return;
|
|
2451
|
-
}
|
|
3242
|
+
if (data) {
|
|
3243
|
+
const result = executor.run([data, count], PER_RECORD_TIMEOUT_MS);
|
|
3244
|
+
if (result.error) {
|
|
3245
|
+
// Don't block the stream on a single bad record — log,
|
|
3246
|
+
// skip, continue. Matches the previous try/catch in the
|
|
3247
|
+
// unsandboxed implementation.
|
|
3248
|
+
console.log(
|
|
3249
|
+
"[transform] mapping error on record " +
|
|
3250
|
+
count +
|
|
3251
|
+
": " +
|
|
3252
|
+
result.error,
|
|
3253
|
+
);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
2452
3256
|
|
|
2453
|
-
|
|
3257
|
+
writeStream.write(sep + JSON.stringify(result.value));
|
|
2454
3258
|
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
3259
|
+
if (callbackEvent !== null && win !== null) {
|
|
3260
|
+
win.webContents.send(callbackEvent, { count });
|
|
3261
|
+
}
|
|
2458
3262
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
3263
|
+
count++;
|
|
3264
|
+
}
|
|
3265
|
+
} catch (e) {
|
|
3266
|
+
console.log(e.message);
|
|
3267
|
+
}
|
|
3268
|
+
});
|
|
2465
3269
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3270
|
+
readStream.on("end", () => {
|
|
3271
|
+
writeStream.write("\n]");
|
|
3272
|
+
writeStream.close();
|
|
3273
|
+
executor.dispose();
|
|
3274
|
+
resolve("Complete: wrote " + count + " objects");
|
|
3275
|
+
});
|
|
2472
3276
|
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
3277
|
+
readStream.on("error", (err) => {
|
|
3278
|
+
console.log("read stream error transform ", err.message);
|
|
3279
|
+
executor.dispose();
|
|
3280
|
+
reject(err);
|
|
3281
|
+
});
|
|
3282
|
+
} // end startStreamingWithExecutor
|
|
3283
|
+
});
|
|
3284
|
+
};
|
|
2481
3285
|
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
3286
|
+
/**
|
|
3287
|
+
* sanitizeLine (line of file)
|
|
3288
|
+
* @param {String} line JSON data that we wish to sanitize
|
|
3289
|
+
* @returns String the sanitized String
|
|
3290
|
+
*/
|
|
3291
|
+
sanitizeLine = (line) => {
|
|
3292
|
+
try {
|
|
3293
|
+
let newLine = line;
|
|
2490
3294
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
3295
|
+
if (line.slice(-1) === ",") {
|
|
3296
|
+
newLine = newLine.slice(0, -1);
|
|
3297
|
+
}
|
|
2494
3298
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
3299
|
+
if (line.slice(0, 1) === "[") {
|
|
3300
|
+
newLine = newLine.slice(1);
|
|
3301
|
+
}
|
|
2498
3302
|
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
}
|
|
3303
|
+
return newLine;
|
|
3304
|
+
} catch (e) {
|
|
3305
|
+
return line;
|
|
3306
|
+
}
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
2505
3309
|
|
|
2506
|
-
|
|
3310
|
+
transform = Transform;
|
|
3311
|
+
return transform;
|
|
3312
|
+
}
|
|
2507
3313
|
|
|
2508
|
-
const { app: app$
|
|
2509
|
-
var fs$
|
|
2510
|
-
const path$
|
|
3314
|
+
const { app: app$a } = require$$0$1;
|
|
3315
|
+
var fs$c = require$$0$2;
|
|
3316
|
+
const path$h = require$$1$2;
|
|
2511
3317
|
const events$5 = events$8;
|
|
2512
3318
|
const { getFileContents: getFileContents$5, writeToFile: writeToFile$2 } = file;
|
|
2513
3319
|
const { safePath: safePath$2, getAllowedRoots: getAllowedRoots$1 } = safePath_1;
|
|
3320
|
+
const { gateFsCall, gateFsCallWithJit } = fsGate;
|
|
3321
|
+
const { readEnforceFlag: readEnforceFlag$1, readJitFlag: readJitFlag$1 } = securityFlags;
|
|
3322
|
+
|
|
3323
|
+
// Reads the enforcement + JIT flags from settings.json. Mirrors the
|
|
3324
|
+
// helper in mcpController. The flag is shared across MCP and fs domains
|
|
3325
|
+
// — see Phase 2 plan for rationale (the cosmetic rename to a
|
|
3326
|
+
// domain-neutral name is a separate slice).
|
|
3327
|
+
function _loadFlags() {
|
|
3328
|
+
try {
|
|
3329
|
+
const settingsPath = path$h.join(
|
|
3330
|
+
app$a.getPath("userData"),
|
|
3331
|
+
appName$5,
|
|
3332
|
+
"settings.json",
|
|
3333
|
+
);
|
|
3334
|
+
if (!fs$c.existsSync(settingsPath)) return null;
|
|
3335
|
+
return JSON.parse(fs$c.readFileSync(settingsPath, "utf8"));
|
|
3336
|
+
} catch (_e) {
|
|
3337
|
+
return null;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
/**
|
|
3342
|
+
* Run the fs gate before a dataController handler does its work.
|
|
3343
|
+
* On deny, sends an error event to the renderer and returns false so
|
|
3344
|
+
* the caller can early-out. On allow, returns true.
|
|
3345
|
+
*
|
|
3346
|
+
* @returns {Promise<boolean>}
|
|
3347
|
+
*/
|
|
3348
|
+
async function _runFsGate(win, action, widgetId, args, errorEvent) {
|
|
3349
|
+
const settings = _loadFlags();
|
|
3350
|
+
if (!readEnforceFlag$1(settings)) return true; // gate disabled
|
|
3351
|
+
if (!widgetId) return true; // legacy callers without widgetId — see plan
|
|
3352
|
+
const gate = readJitFlag$1(settings)
|
|
3353
|
+
? await gateFsCallWithJit({ widgetId, action, args }, { enableJit: true })
|
|
3354
|
+
: gateFsCall({ widgetId, action, args });
|
|
3355
|
+
if (gate.allow) return true;
|
|
3356
|
+
if (win && errorEvent) {
|
|
3357
|
+
win.webContents.send(errorEvent, {
|
|
3358
|
+
success: false,
|
|
3359
|
+
message: "fs permission gate: " + gate.reason,
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
return false;
|
|
3363
|
+
}
|
|
2514
3364
|
|
|
2515
3365
|
// Convert Json to Csv
|
|
2516
|
-
const ObjectsToCsv = require$$
|
|
2517
|
-
const Transform =
|
|
2518
|
-
const https$3 = require$$
|
|
3366
|
+
const ObjectsToCsv = require$$8$1;
|
|
3367
|
+
const Transform = requireTransform();
|
|
3368
|
+
const https$3 = require$$10;
|
|
2519
3369
|
const appName$5 = "Dashboard";
|
|
2520
3370
|
|
|
2521
3371
|
const dataController$1 = {
|
|
@@ -2532,8 +3382,8 @@ const dataController$1 = {
|
|
|
2532
3382
|
// Validate the renderer-supplied filename is contained within
|
|
2533
3383
|
// the data directory. path.join doesn't reject `..` segments;
|
|
2534
3384
|
// safePath does.
|
|
2535
|
-
const candidate = path$
|
|
2536
|
-
app$
|
|
3385
|
+
const candidate = path$h.join(
|
|
3386
|
+
app$a.getPath("userData"),
|
|
2537
3387
|
appName$5,
|
|
2538
3388
|
appId,
|
|
2539
3389
|
"data",
|
|
@@ -2691,7 +3541,7 @@ const dataController$1 = {
|
|
|
2691
3541
|
// intent, plus realpath/symlink protection.
|
|
2692
3542
|
const resolvedFilepath = safePath$2(toFilepath, getAllowedRoots$1("data"));
|
|
2693
3543
|
|
|
2694
|
-
const writeStream = fs$
|
|
3544
|
+
const writeStream = fs$c.createWriteStream(resolvedFilepath);
|
|
2695
3545
|
|
|
2696
3546
|
https$3
|
|
2697
3547
|
.get(url, (resp) => {
|
|
@@ -2850,13 +3700,31 @@ const dataController$1 = {
|
|
|
2850
3700
|
* @param {*} append
|
|
2851
3701
|
* @param {*} returnEmpty
|
|
2852
3702
|
*/
|
|
2853
|
-
saveToFile: (
|
|
3703
|
+
saveToFile: async (
|
|
3704
|
+
win,
|
|
3705
|
+
data,
|
|
3706
|
+
filename,
|
|
3707
|
+
append,
|
|
3708
|
+
returnEmpty = {},
|
|
3709
|
+
widgetId = null,
|
|
3710
|
+
) => {
|
|
3711
|
+
// Phase 2 fs gate. Runs before safePath containment so JIT can
|
|
3712
|
+
// prompt the user without leaking path-shape information through
|
|
3713
|
+
// error timing. See electron/security/fsGate.js.
|
|
3714
|
+
const gateOk = await _runFsGate(
|
|
3715
|
+
win,
|
|
3716
|
+
"saveToFile",
|
|
3717
|
+
widgetId,
|
|
3718
|
+
{ filename },
|
|
3719
|
+
events$5.DATA_SAVE_TO_FILE_ERROR,
|
|
3720
|
+
);
|
|
3721
|
+
if (!gateOk) return;
|
|
2854
3722
|
try {
|
|
2855
3723
|
if (data) {
|
|
2856
3724
|
// Validate filename is contained within the data directory.
|
|
2857
3725
|
// path.join doesn't reject `..` segments; safePath does.
|
|
2858
|
-
const candidate = path$
|
|
2859
|
-
app$
|
|
3726
|
+
const candidate = path$h.join(
|
|
3727
|
+
app$a.getPath("userData"),
|
|
2860
3728
|
appName$5,
|
|
2861
3729
|
"data",
|
|
2862
3730
|
filename,
|
|
@@ -2944,12 +3812,21 @@ const dataController$1 = {
|
|
|
2944
3812
|
}
|
|
2945
3813
|
},
|
|
2946
3814
|
|
|
2947
|
-
readFromFile: (win, filename, returnIfEmpty = {}) => {
|
|
3815
|
+
readFromFile: async (win, filename, returnIfEmpty = {}, widgetId = null) => {
|
|
3816
|
+
// Phase 2 fs gate — same as saveToFile.
|
|
3817
|
+
const gateOk = await _runFsGate(
|
|
3818
|
+
win,
|
|
3819
|
+
"readFromFile",
|
|
3820
|
+
widgetId,
|
|
3821
|
+
{ filename },
|
|
3822
|
+
events$5.DATA_READ_FROM_FILE_ERROR,
|
|
3823
|
+
);
|
|
3824
|
+
if (!gateOk) return;
|
|
2948
3825
|
try {
|
|
2949
3826
|
if (filename) {
|
|
2950
3827
|
// filename to the pages file (live pages)
|
|
2951
|
-
const fromFilename = path$
|
|
2952
|
-
app$
|
|
3828
|
+
const fromFilename = path$h.join(
|
|
3829
|
+
app$a.getPath("userData"),
|
|
2953
3830
|
appName$5,
|
|
2954
3831
|
"data",
|
|
2955
3832
|
filename,
|
|
@@ -3029,9 +3906,9 @@ var dataController_1 = dataController$1;
|
|
|
3029
3906
|
* settingsController
|
|
3030
3907
|
*/
|
|
3031
3908
|
|
|
3032
|
-
const { app: app$
|
|
3033
|
-
const path$
|
|
3034
|
-
const fs$
|
|
3909
|
+
const { app: app$9 } = require$$0$1;
|
|
3910
|
+
const path$g = require$$1$2;
|
|
3911
|
+
const fs$b = require$$0$2;
|
|
3035
3912
|
const { getFileContents: getFileContents$4, writeToFile: writeToFile$1 } = file;
|
|
3036
3913
|
|
|
3037
3914
|
const configFilename$3 = "settings.json";
|
|
@@ -3039,15 +3916,15 @@ const appName$4 = "Dashboard";
|
|
|
3039
3916
|
|
|
3040
3917
|
// Helper function to recursively copy directory
|
|
3041
3918
|
function copyDirectory(source, destination) {
|
|
3042
|
-
if (!fs$
|
|
3043
|
-
fs$
|
|
3919
|
+
if (!fs$b.existsSync(destination)) {
|
|
3920
|
+
fs$b.mkdirSync(destination, { recursive: true });
|
|
3044
3921
|
}
|
|
3045
3922
|
|
|
3046
|
-
const files = fs$
|
|
3923
|
+
const files = fs$b.readdirSync(source);
|
|
3047
3924
|
for (const file of files) {
|
|
3048
|
-
const srcPath = path$
|
|
3049
|
-
const destPath = path$
|
|
3050
|
-
const stat = fs$
|
|
3925
|
+
const srcPath = path$g.join(source, file);
|
|
3926
|
+
const destPath = path$g.join(destination, file);
|
|
3927
|
+
const stat = fs$b.lstatSync(srcPath);
|
|
3051
3928
|
|
|
3052
3929
|
// Skip symlinks to prevent following links to sensitive files
|
|
3053
3930
|
if (stat.isSymbolicLink()) {
|
|
@@ -3058,7 +3935,7 @@ function copyDirectory(source, destination) {
|
|
|
3058
3935
|
if (stat.isDirectory()) {
|
|
3059
3936
|
copyDirectory(srcPath, destPath);
|
|
3060
3937
|
} else {
|
|
3061
|
-
fs$
|
|
3938
|
+
fs$b.copyFileSync(srcPath, destPath);
|
|
3062
3939
|
}
|
|
3063
3940
|
}
|
|
3064
3941
|
}
|
|
@@ -3074,8 +3951,8 @@ const settingsController$4 = {
|
|
|
3074
3951
|
try {
|
|
3075
3952
|
if (data) {
|
|
3076
3953
|
// <appId>/settings.json
|
|
3077
|
-
const filename = path$
|
|
3078
|
-
app$
|
|
3954
|
+
const filename = path$g.join(
|
|
3955
|
+
app$9.getPath("userData"),
|
|
3079
3956
|
appName$4,
|
|
3080
3957
|
configFilename$3,
|
|
3081
3958
|
);
|
|
@@ -3110,8 +3987,8 @@ const settingsController$4 = {
|
|
|
3110
3987
|
getSettingsForApplication: (win) => {
|
|
3111
3988
|
try {
|
|
3112
3989
|
// <appId>/settings.json
|
|
3113
|
-
const filename = path$
|
|
3114
|
-
app$
|
|
3990
|
+
const filename = path$g.join(
|
|
3991
|
+
app$9.getPath("userData"),
|
|
3115
3992
|
appName$4,
|
|
3116
3993
|
configFilename$3,
|
|
3117
3994
|
);
|
|
@@ -3141,15 +4018,15 @@ const settingsController$4 = {
|
|
|
3141
4018
|
*/
|
|
3142
4019
|
getDataDirectory: (win) => {
|
|
3143
4020
|
try {
|
|
3144
|
-
const settingsPath = path$
|
|
3145
|
-
app$
|
|
4021
|
+
const settingsPath = path$g.join(
|
|
4022
|
+
app$9.getPath("userData"),
|
|
3146
4023
|
appName$4,
|
|
3147
4024
|
configFilename$3,
|
|
3148
4025
|
);
|
|
3149
4026
|
const settings = getFileContents$4(settingsPath, {});
|
|
3150
4027
|
const userDataDir =
|
|
3151
4028
|
settings.userDataDirectory ||
|
|
3152
|
-
path$
|
|
4029
|
+
path$g.join(app$9.getPath("userData"), appName$4);
|
|
3153
4030
|
|
|
3154
4031
|
console.log("[settingsController] Data directory retrieved successfully");
|
|
3155
4032
|
// Return the data for ipcMain.handle() - modern promise-based approach
|
|
@@ -3176,18 +4053,18 @@ const settingsController$4 = {
|
|
|
3176
4053
|
setDataDirectory: (win, newPath) => {
|
|
3177
4054
|
try {
|
|
3178
4055
|
// Validate the path exists and is a directory
|
|
3179
|
-
if (!fs$
|
|
3180
|
-
fs$
|
|
4056
|
+
if (!fs$b.existsSync(newPath)) {
|
|
4057
|
+
fs$b.mkdirSync(newPath, { recursive: true });
|
|
3181
4058
|
}
|
|
3182
4059
|
|
|
3183
|
-
const stats = fs$
|
|
4060
|
+
const stats = fs$b.statSync(newPath);
|
|
3184
4061
|
if (!stats.isDirectory()) {
|
|
3185
4062
|
throw new Error("Path is not a directory");
|
|
3186
4063
|
}
|
|
3187
4064
|
|
|
3188
4065
|
// Update settings
|
|
3189
|
-
const settingsPath = path$
|
|
3190
|
-
app$
|
|
4066
|
+
const settingsPath = path$g.join(
|
|
4067
|
+
app$9.getPath("userData"),
|
|
3191
4068
|
appName$4,
|
|
3192
4069
|
configFilename$3,
|
|
3193
4070
|
);
|
|
@@ -3220,20 +4097,20 @@ const settingsController$4 = {
|
|
|
3220
4097
|
migrateDataDirectory: (win, oldPath, newPath) => {
|
|
3221
4098
|
try {
|
|
3222
4099
|
// Resolve paths to prevent traversal
|
|
3223
|
-
const resolvedOldPath = path$
|
|
3224
|
-
const resolvedNewPath = path$
|
|
4100
|
+
const resolvedOldPath = path$g.resolve(oldPath);
|
|
4101
|
+
const resolvedNewPath = path$g.resolve(newPath);
|
|
3225
4102
|
|
|
3226
4103
|
// Validate oldPath is the current configured data directory
|
|
3227
|
-
const settingsCheckPath = path$
|
|
3228
|
-
app$
|
|
4104
|
+
const settingsCheckPath = path$g.join(
|
|
4105
|
+
app$9.getPath("userData"),
|
|
3229
4106
|
appName$4,
|
|
3230
4107
|
configFilename$3,
|
|
3231
4108
|
);
|
|
3232
4109
|
const currentSettings = getFileContents$4(settingsCheckPath, {});
|
|
3233
4110
|
const currentDataDir =
|
|
3234
4111
|
currentSettings.userDataDirectory ||
|
|
3235
|
-
path$
|
|
3236
|
-
if (resolvedOldPath !== path$
|
|
4112
|
+
path$g.join(app$9.getPath("userData"), appName$4);
|
|
4113
|
+
if (resolvedOldPath !== path$g.resolve(currentDataDir)) {
|
|
3237
4114
|
throw new Error("Source path must be the current data directory");
|
|
3238
4115
|
}
|
|
3239
4116
|
|
|
@@ -3257,20 +4134,20 @@ const settingsController$4 = {
|
|
|
3257
4134
|
}
|
|
3258
4135
|
|
|
3259
4136
|
// Validate paths
|
|
3260
|
-
if (!fs$
|
|
4137
|
+
if (!fs$b.existsSync(resolvedOldPath)) {
|
|
3261
4138
|
throw new Error("Source directory does not exist");
|
|
3262
4139
|
}
|
|
3263
4140
|
|
|
3264
|
-
if (!fs$
|
|
3265
|
-
fs$
|
|
4141
|
+
if (!fs$b.existsSync(resolvedNewPath)) {
|
|
4142
|
+
fs$b.mkdirSync(resolvedNewPath, { recursive: true });
|
|
3266
4143
|
}
|
|
3267
4144
|
|
|
3268
4145
|
// Copy files
|
|
3269
4146
|
copyDirectory(resolvedOldPath, resolvedNewPath);
|
|
3270
4147
|
|
|
3271
4148
|
// Update settings to use new path
|
|
3272
|
-
const settingsPath = path$
|
|
3273
|
-
app$
|
|
4149
|
+
const settingsPath = path$g.join(
|
|
4150
|
+
app$9.getPath("userData"),
|
|
3274
4151
|
appName$4,
|
|
3275
4152
|
configFilename$3,
|
|
3276
4153
|
);
|
|
@@ -3934,8 +4811,8 @@ function requireProviderController () {
|
|
|
3934
4811
|
return providerController_1;
|
|
3935
4812
|
}
|
|
3936
4813
|
|
|
3937
|
-
const { app: app$
|
|
3938
|
-
const path$
|
|
4814
|
+
const { app: app$8 } = require$$0$1;
|
|
4815
|
+
const path$f = require$$1$2;
|
|
3939
4816
|
const { writeFileSync: writeFileSync$1 } = require$$0$2;
|
|
3940
4817
|
const events$4 = events$8;
|
|
3941
4818
|
const { getFileContents: getFileContents$3 } = file;
|
|
@@ -3955,8 +4832,8 @@ const layoutController$1 = {
|
|
|
3955
4832
|
saveLayoutForApplication: (win, appId, layoutObject) => {
|
|
3956
4833
|
try {
|
|
3957
4834
|
// filename to the pages file (live pages)
|
|
3958
|
-
const filename = path$
|
|
3959
|
-
app$
|
|
4835
|
+
const filename = path$f.join(
|
|
4836
|
+
app$8.getPath("userData"),
|
|
3960
4837
|
appName$3,
|
|
3961
4838
|
appId,
|
|
3962
4839
|
configFilename$2,
|
|
@@ -3988,8 +4865,8 @@ const layoutController$1 = {
|
|
|
3988
4865
|
*/
|
|
3989
4866
|
listLayoutsForApplication: (win, appId) => {
|
|
3990
4867
|
try {
|
|
3991
|
-
const filename = path$
|
|
3992
|
-
app$
|
|
4868
|
+
const filename = path$f.join(
|
|
4869
|
+
app$8.getPath("userData"),
|
|
3993
4870
|
appName$3,
|
|
3994
4871
|
appId,
|
|
3995
4872
|
configFilename$2,
|
|
@@ -21152,455 +22029,6 @@ let StreamableHTTPClientTransport$1 = class StreamableHTTPClientTransport {
|
|
|
21152
22029
|
};
|
|
21153
22030
|
streamableHttp$1.StreamableHTTPClientTransport = StreamableHTTPClientTransport$1;
|
|
21154
22031
|
|
|
21155
|
-
/**
|
|
21156
|
-
* grantedPermissions.js
|
|
21157
|
-
*
|
|
21158
|
-
* Stores the user's actual MCP permission grants per widget. This is the
|
|
21159
|
-
* Slice-2 enforcement source of truth — separate from the widget's declared
|
|
21160
|
-
* `dash.permissions.mcp` block (which is just a request).
|
|
21161
|
-
*
|
|
21162
|
-
* The runtime gate (permissionGate.gateToolCall) reads from here only.
|
|
21163
|
-
* A widget with a declared manifest but no grant entry has no access:
|
|
21164
|
-
* fail-closed. The user grants permissions at install time (consent modal)
|
|
21165
|
-
* or later in Settings → Privacy & Security.
|
|
21166
|
-
*
|
|
21167
|
-
* Storage: userData/widgetMcpGrants.json. Atomic writes via tmp + rename.
|
|
21168
|
-
*
|
|
21169
|
-
* Shape on disk:
|
|
21170
|
-
* {
|
|
21171
|
-
* "@trops/notes-summarizer": {
|
|
21172
|
-
* "servers": {
|
|
21173
|
-
* "filesystem": {
|
|
21174
|
-
* "tools": ["read_file"],
|
|
21175
|
-
* "readPaths": ["/Users/jane/Documents/notes"],
|
|
21176
|
-
* "writePaths": []
|
|
21177
|
-
* }
|
|
21178
|
-
* }
|
|
21179
|
-
* }
|
|
21180
|
-
* }
|
|
21181
|
-
*
|
|
21182
|
-
* Note: paths are stored as-is (already tilde-expanded by the manifest
|
|
21183
|
-
* parser before grants are written). Tests can re-expand via
|
|
21184
|
-
* widgetPermissions.expandHome if they store ~ literals.
|
|
21185
|
-
*
|
|
21186
|
-
* Public API:
|
|
21187
|
-
* getGrant(widgetId) → grant | null
|
|
21188
|
-
* setGrant(widgetId, perms) → boolean
|
|
21189
|
-
* revokeGrant(widgetId) → boolean
|
|
21190
|
-
* revokeServer(widgetId, serverName) → boolean
|
|
21191
|
-
* listAllGrants() → [{ widgetId, granted }]
|
|
21192
|
-
* clearCache() → void // test-only
|
|
21193
|
-
*/
|
|
21194
|
-
|
|
21195
|
-
const fs$b = require$$0$2;
|
|
21196
|
-
const path$f = require$$1$2;
|
|
21197
|
-
const { app: app$8 } = require$$0$1;
|
|
21198
|
-
|
|
21199
|
-
const FILE_NAME = "widgetMcpGrants.json";
|
|
21200
|
-
|
|
21201
|
-
// In-process cache of the entire grants file. Lazily loaded; invalidated
|
|
21202
|
-
// on every write.
|
|
21203
|
-
let _cache$1 = null;
|
|
21204
|
-
|
|
21205
|
-
function grantsFilePath() {
|
|
21206
|
-
return path$f.join(app$8.getPath("userData"), FILE_NAME);
|
|
21207
|
-
}
|
|
21208
|
-
|
|
21209
|
-
function loadFromDisk() {
|
|
21210
|
-
const p = grantsFilePath();
|
|
21211
|
-
if (!fs$b.existsSync(p)) return {};
|
|
21212
|
-
try {
|
|
21213
|
-
const raw = fs$b.readFileSync(p, "utf8");
|
|
21214
|
-
const parsed = JSON.parse(raw);
|
|
21215
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
21216
|
-
return parsed;
|
|
21217
|
-
} catch (e) {
|
|
21218
|
-
console.warn("[grantedPermissions] failed to read " + p + ": " + e.message);
|
|
21219
|
-
return {};
|
|
21220
|
-
}
|
|
21221
|
-
}
|
|
21222
|
-
|
|
21223
|
-
function ensureCache() {
|
|
21224
|
-
if (_cache$1 === null) _cache$1 = loadFromDisk();
|
|
21225
|
-
return _cache$1;
|
|
21226
|
-
}
|
|
21227
|
-
|
|
21228
|
-
function writeToDisk(data) {
|
|
21229
|
-
const p = grantsFilePath();
|
|
21230
|
-
const tmp = p + ".tmp";
|
|
21231
|
-
// Ensure parent dir exists (userData should already, but be defensive
|
|
21232
|
-
// for first-launch / freshly-cleared profile cases).
|
|
21233
|
-
fs$b.mkdirSync(path$f.dirname(p), { recursive: true });
|
|
21234
|
-
fs$b.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
21235
|
-
fs$b.renameSync(tmp, p);
|
|
21236
|
-
}
|
|
21237
|
-
|
|
21238
|
-
// Recognized origins for a persisted grant.
|
|
21239
|
-
// "declared" — user approved against the developer's declared
|
|
21240
|
-
// dash.permissions.mcp block at install time.
|
|
21241
|
-
// "discovered" — install-time scanner produced a synthetic manifest
|
|
21242
|
-
// the user approved.
|
|
21243
|
-
// "manual" — user typed entries themselves in
|
|
21244
|
-
// Settings → Privacy & Security with no manifest backing.
|
|
21245
|
-
// "live" — user approved a just-in-time consent prompt at
|
|
21246
|
-
// runtime when a tool call hit the gate without a
|
|
21247
|
-
// matching grant.
|
|
21248
|
-
// Other values are dropped on persist (legacy grants stay null).
|
|
21249
|
-
const ALLOWED_GRANT_ORIGINS = new Set([
|
|
21250
|
-
"declared",
|
|
21251
|
-
"discovered",
|
|
21252
|
-
"manual",
|
|
21253
|
-
"live",
|
|
21254
|
-
]);
|
|
21255
|
-
|
|
21256
|
-
/**
|
|
21257
|
-
* Sanitize a perms object before persisting. Drops unknown keys, coerces
|
|
21258
|
-
* arrays of strings, and silently ignores malformed servers. Mirrors the
|
|
21259
|
-
* shape produced by parseManifestPermissions so the gate reads either
|
|
21260
|
-
* declared or granted with the same code path.
|
|
21261
|
-
*
|
|
21262
|
-
* Optional `grantOrigin` field is preserved when it's one of the
|
|
21263
|
-
* recognized values; bogus values are dropped.
|
|
21264
|
-
*/
|
|
21265
|
-
function sanitizePerms(perms) {
|
|
21266
|
-
if (!perms || typeof perms !== "object") return null;
|
|
21267
|
-
const rawServers =
|
|
21268
|
-
perms.servers && typeof perms.servers === "object" ? perms.servers : null;
|
|
21269
|
-
if (!rawServers) return null;
|
|
21270
|
-
|
|
21271
|
-
const servers = {};
|
|
21272
|
-
for (const [name, raw] of Object.entries(rawServers)) {
|
|
21273
|
-
if (!raw || typeof raw !== "object") continue;
|
|
21274
|
-
servers[name] = {
|
|
21275
|
-
tools: Array.isArray(raw.tools)
|
|
21276
|
-
? raw.tools.filter((t) => typeof t === "string")
|
|
21277
|
-
: [],
|
|
21278
|
-
readPaths: Array.isArray(raw.readPaths)
|
|
21279
|
-
? raw.readPaths.filter((p) => typeof p === "string")
|
|
21280
|
-
: [],
|
|
21281
|
-
writePaths: Array.isArray(raw.writePaths)
|
|
21282
|
-
? raw.writePaths.filter((p) => typeof p === "string")
|
|
21283
|
-
: [],
|
|
21284
|
-
};
|
|
21285
|
-
}
|
|
21286
|
-
const out = { servers };
|
|
21287
|
-
if (
|
|
21288
|
-
typeof perms.grantOrigin === "string" &&
|
|
21289
|
-
ALLOWED_GRANT_ORIGINS.has(perms.grantOrigin)
|
|
21290
|
-
) {
|
|
21291
|
-
out.grantOrigin = perms.grantOrigin;
|
|
21292
|
-
}
|
|
21293
|
-
return out;
|
|
21294
|
-
}
|
|
21295
|
-
|
|
21296
|
-
function getGrant$2(widgetId) {
|
|
21297
|
-
if (typeof widgetId !== "string" || !widgetId) return null;
|
|
21298
|
-
const all = ensureCache();
|
|
21299
|
-
return all[widgetId] || null;
|
|
21300
|
-
}
|
|
21301
|
-
|
|
21302
|
-
function setGrant$2(widgetId, perms) {
|
|
21303
|
-
if (typeof widgetId !== "string" || !widgetId) return false;
|
|
21304
|
-
const sanitized = sanitizePerms(perms);
|
|
21305
|
-
if (!sanitized) return false;
|
|
21306
|
-
const all = ensureCache();
|
|
21307
|
-
all[widgetId] = sanitized;
|
|
21308
|
-
try {
|
|
21309
|
-
writeToDisk(all);
|
|
21310
|
-
return true;
|
|
21311
|
-
} catch (e) {
|
|
21312
|
-
console.warn(
|
|
21313
|
-
"[grantedPermissions] failed to write grant for " +
|
|
21314
|
-
widgetId +
|
|
21315
|
-
": " +
|
|
21316
|
-
e.message,
|
|
21317
|
-
);
|
|
21318
|
-
// Roll back the cache entry so memory matches disk.
|
|
21319
|
-
_cache$1 = loadFromDisk();
|
|
21320
|
-
return false;
|
|
21321
|
-
}
|
|
21322
|
-
}
|
|
21323
|
-
|
|
21324
|
-
function revokeGrant$1(widgetId) {
|
|
21325
|
-
if (typeof widgetId !== "string" || !widgetId) return false;
|
|
21326
|
-
const all = ensureCache();
|
|
21327
|
-
if (!Object.prototype.hasOwnProperty.call(all, widgetId)) return false;
|
|
21328
|
-
delete all[widgetId];
|
|
21329
|
-
try {
|
|
21330
|
-
writeToDisk(all);
|
|
21331
|
-
return true;
|
|
21332
|
-
} catch (e) {
|
|
21333
|
-
console.warn(
|
|
21334
|
-
"[grantedPermissions] failed to revoke grant for " +
|
|
21335
|
-
widgetId +
|
|
21336
|
-
": " +
|
|
21337
|
-
e.message,
|
|
21338
|
-
);
|
|
21339
|
-
_cache$1 = loadFromDisk();
|
|
21340
|
-
return false;
|
|
21341
|
-
}
|
|
21342
|
-
}
|
|
21343
|
-
|
|
21344
|
-
function revokeServer$1(widgetId, serverName) {
|
|
21345
|
-
if (typeof widgetId !== "string" || !widgetId) return false;
|
|
21346
|
-
if (typeof serverName !== "string" || !serverName) return false;
|
|
21347
|
-
const all = ensureCache();
|
|
21348
|
-
const widgetEntry = all[widgetId];
|
|
21349
|
-
if (!widgetEntry || !widgetEntry.servers) return false;
|
|
21350
|
-
if (!Object.prototype.hasOwnProperty.call(widgetEntry.servers, serverName))
|
|
21351
|
-
return false;
|
|
21352
|
-
delete widgetEntry.servers[serverName];
|
|
21353
|
-
try {
|
|
21354
|
-
writeToDisk(all);
|
|
21355
|
-
return true;
|
|
21356
|
-
} catch (e) {
|
|
21357
|
-
console.warn(
|
|
21358
|
-
"[grantedPermissions] failed to revoke server " +
|
|
21359
|
-
serverName +
|
|
21360
|
-
" for " +
|
|
21361
|
-
widgetId +
|
|
21362
|
-
": " +
|
|
21363
|
-
e.message,
|
|
21364
|
-
);
|
|
21365
|
-
_cache$1 = loadFromDisk();
|
|
21366
|
-
return false;
|
|
21367
|
-
}
|
|
21368
|
-
}
|
|
21369
|
-
|
|
21370
|
-
function listAllGrants$1() {
|
|
21371
|
-
const all = ensureCache();
|
|
21372
|
-
return Object.entries(all).map(([widgetId, granted]) => ({
|
|
21373
|
-
widgetId,
|
|
21374
|
-
granted,
|
|
21375
|
-
}));
|
|
21376
|
-
}
|
|
21377
|
-
|
|
21378
|
-
function clearCache$1() {
|
|
21379
|
-
_cache$1 = null;
|
|
21380
|
-
}
|
|
21381
|
-
|
|
21382
|
-
var grantedPermissions = {
|
|
21383
|
-
getGrant: getGrant$2,
|
|
21384
|
-
setGrant: setGrant$2,
|
|
21385
|
-
revokeGrant: revokeGrant$1,
|
|
21386
|
-
revokeServer: revokeServer$1,
|
|
21387
|
-
listAllGrants: listAllGrants$1,
|
|
21388
|
-
clearCache: clearCache$1,
|
|
21389
|
-
ALLOWED_GRANT_ORIGINS,
|
|
21390
|
-
};
|
|
21391
|
-
|
|
21392
|
-
/**
|
|
21393
|
-
* jitConsent.js
|
|
21394
|
-
*
|
|
21395
|
-
* Just-in-time permission consent for widget→backend calls.
|
|
21396
|
-
*
|
|
21397
|
-
* When a widget hits a gate without an existing grant for the requested
|
|
21398
|
-
* (domain, action, args), the gate calls `requestApproval` which:
|
|
21399
|
-
* 1. Synchronously emits `widget:permission-required` to all
|
|
21400
|
-
* BrowserWindows with a unique requestId.
|
|
21401
|
-
* 2. Returns a Promise that resolves on user response or rejects on
|
|
21402
|
-
* timeout.
|
|
21403
|
-
* 3. Coalesces requests with the same coalescing key so a widget
|
|
21404
|
-
* bursting identical calls produces one prompt, not many.
|
|
21405
|
-
*
|
|
21406
|
-
* The renderer's JitConsentModal subscribes to the event, presents the
|
|
21407
|
-
* user with granularity options (this once / this tool / this tool +
|
|
21408
|
-
* parent dir), and replies via `widget:permission-response` with
|
|
21409
|
-
* `{ requestId, decision }`. main.js wires the IPC handler back to
|
|
21410
|
-
* `_handleResponse`.
|
|
21411
|
-
*
|
|
21412
|
-
* The module is intentionally domain-agnostic in shape — the request
|
|
21413
|
-
* payload carries `domain` so future plug-ins (fs, algolia, llm) reuse
|
|
21414
|
-
* the same machinery. Phase 1 only emits with `domain: "mcp"`.
|
|
21415
|
-
*
|
|
21416
|
-
* Public surface:
|
|
21417
|
-
* requestApproval(req, opts) → Promise<{ approve, scope?, ... }>
|
|
21418
|
-
* _handleResponse({ requestId, decision }) → void (called from main.js IPC)
|
|
21419
|
-
* _resetForTest() → void (test-only)
|
|
21420
|
-
*/
|
|
21421
|
-
|
|
21422
|
-
const { BrowserWindow: BrowserWindow$2, ipcMain: ipcMain$2 } = require$$0$1;
|
|
21423
|
-
|
|
21424
|
-
const REQUEST_CHANNEL = "widget:permission-required";
|
|
21425
|
-
const RESPONSE_CHANNEL = "widget:permission-response";
|
|
21426
|
-
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
21427
|
-
|
|
21428
|
-
// requestId → { resolve, reject, timeout, coalesceKey, joinedResolvers }
|
|
21429
|
-
const _pending = new Map();
|
|
21430
|
-
// coalesceKey → requestId (so duplicate requests join the live one)
|
|
21431
|
-
const _coalesce = new Map();
|
|
21432
|
-
let _idCounter = 0;
|
|
21433
|
-
|
|
21434
|
-
function nextRequestId() {
|
|
21435
|
-
_idCounter += 1;
|
|
21436
|
-
return `jit-${Date.now()}-${_idCounter}`;
|
|
21437
|
-
}
|
|
21438
|
-
|
|
21439
|
-
/**
|
|
21440
|
-
* Build a coalescing key from the request. Two requests share the same
|
|
21441
|
-
* key iff they're "the same prompt" — same widget, same domain+action,
|
|
21442
|
-
* same target server/tool. Args beyond that (e.g. exact path) DON'T
|
|
21443
|
-
* differentiate; if the user is being asked about read_file already,
|
|
21444
|
-
* approving handles all current paths.
|
|
21445
|
-
*/
|
|
21446
|
-
function coalesceKeyOf(req) {
|
|
21447
|
-
if (req.domain === "mcp") {
|
|
21448
|
-
const innerArgs = req.args || {};
|
|
21449
|
-
return [
|
|
21450
|
-
req.widgetId,
|
|
21451
|
-
"mcp",
|
|
21452
|
-
innerArgs.serverName || "",
|
|
21453
|
-
innerArgs.toolName || "",
|
|
21454
|
-
].join("::");
|
|
21455
|
-
}
|
|
21456
|
-
// Default: domain + action + serialized top-level args
|
|
21457
|
-
return [
|
|
21458
|
-
req.widgetId,
|
|
21459
|
-
req.domain,
|
|
21460
|
-
req.action,
|
|
21461
|
-
JSON.stringify(req.args || {}),
|
|
21462
|
-
].join("::");
|
|
21463
|
-
}
|
|
21464
|
-
|
|
21465
|
-
function emitEvent(payload) {
|
|
21466
|
-
let wins = [];
|
|
21467
|
-
try {
|
|
21468
|
-
wins = BrowserWindow$2.getAllWindows() || [];
|
|
21469
|
-
} catch {
|
|
21470
|
-
wins = [];
|
|
21471
|
-
}
|
|
21472
|
-
for (const w of wins) {
|
|
21473
|
-
try {
|
|
21474
|
-
w?.webContents?.send?.(REQUEST_CHANNEL, payload);
|
|
21475
|
-
} catch {
|
|
21476
|
-
// best-effort broadcast
|
|
21477
|
-
}
|
|
21478
|
-
}
|
|
21479
|
-
}
|
|
21480
|
-
|
|
21481
|
-
function validateRequest(req) {
|
|
21482
|
-
if (!req || typeof req !== "object") return "invalid request: not an object";
|
|
21483
|
-
if (typeof req.widgetId !== "string" || !req.widgetId)
|
|
21484
|
-
return "invalid request: widgetId required";
|
|
21485
|
-
if (typeof req.domain !== "string" || !req.domain)
|
|
21486
|
-
return "invalid request: domain required";
|
|
21487
|
-
if (typeof req.action !== "string" || !req.action)
|
|
21488
|
-
return "invalid request: action required";
|
|
21489
|
-
return null;
|
|
21490
|
-
}
|
|
21491
|
-
|
|
21492
|
-
/**
|
|
21493
|
-
* Request user approval for an out-of-grant call. Returns a promise
|
|
21494
|
-
* that resolves with the user's decision or rejects on timeout / bad
|
|
21495
|
-
* input.
|
|
21496
|
-
*
|
|
21497
|
-
* decision shape (resolved value):
|
|
21498
|
-
* { approve: true, scope: "once" | "tool" | "parent" | "custom", ...extras }
|
|
21499
|
-
* { approve: false, reason?: string }
|
|
21500
|
-
*
|
|
21501
|
-
* `scope` informs the caller how to write the resulting grant.
|
|
21502
|
-
*/
|
|
21503
|
-
function requestApproval$1(req, opts = {}) {
|
|
21504
|
-
const validation = validateRequest(req);
|
|
21505
|
-
if (validation) {
|
|
21506
|
-
return Promise.reject(new Error(validation));
|
|
21507
|
-
}
|
|
21508
|
-
|
|
21509
|
-
const timeoutMs = Number.isFinite(opts.timeoutMs)
|
|
21510
|
-
? opts.timeoutMs
|
|
21511
|
-
: DEFAULT_TIMEOUT_MS;
|
|
21512
|
-
|
|
21513
|
-
// If a prompt for the same coalesce key is already pending, join it.
|
|
21514
|
-
const key = coalesceKeyOf(req);
|
|
21515
|
-
if (_coalesce.has(key)) {
|
|
21516
|
-
const existingId = _coalesce.get(key);
|
|
21517
|
-
const existing = _pending.get(existingId);
|
|
21518
|
-
if (existing) {
|
|
21519
|
-
return new Promise((resolve, reject) => {
|
|
21520
|
-
existing.joinedResolvers.push({ resolve, reject });
|
|
21521
|
-
});
|
|
21522
|
-
}
|
|
21523
|
-
// Stale coalesce entry; drop and fall through to a fresh request.
|
|
21524
|
-
_coalesce.delete(key);
|
|
21525
|
-
}
|
|
21526
|
-
|
|
21527
|
-
return new Promise((resolve, reject) => {
|
|
21528
|
-
const requestId = nextRequestId();
|
|
21529
|
-
const timeout = setTimeout(() => {
|
|
21530
|
-
const entry = _pending.get(requestId);
|
|
21531
|
-
if (!entry) return;
|
|
21532
|
-
_pending.delete(requestId);
|
|
21533
|
-
_coalesce.delete(entry.coalesceKey);
|
|
21534
|
-
const err = new Error(
|
|
21535
|
-
`JIT consent timed out for ${req.widgetId} (${req.domain}/${req.action}) after ${timeoutMs}ms`,
|
|
21536
|
-
);
|
|
21537
|
-
reject(err);
|
|
21538
|
-
for (const j of entry.joinedResolvers) j.reject(err);
|
|
21539
|
-
}, timeoutMs);
|
|
21540
|
-
|
|
21541
|
-
_pending.set(requestId, {
|
|
21542
|
-
resolve,
|
|
21543
|
-
reject,
|
|
21544
|
-
timeout,
|
|
21545
|
-
coalesceKey: key,
|
|
21546
|
-
joinedResolvers: [],
|
|
21547
|
-
});
|
|
21548
|
-
_coalesce.set(key, requestId);
|
|
21549
|
-
|
|
21550
|
-
emitEvent({
|
|
21551
|
-
requestId,
|
|
21552
|
-
widgetId: req.widgetId,
|
|
21553
|
-
domain: req.domain,
|
|
21554
|
-
action: req.action,
|
|
21555
|
-
args: req.args || {},
|
|
21556
|
-
});
|
|
21557
|
-
});
|
|
21558
|
-
}
|
|
21559
|
-
|
|
21560
|
-
function _handleResponse({ requestId, decision } = {}) {
|
|
21561
|
-
if (!requestId || typeof requestId !== "string") return;
|
|
21562
|
-
const entry = _pending.get(requestId);
|
|
21563
|
-
if (!entry) return; // unknown request — drop silently
|
|
21564
|
-
clearTimeout(entry.timeout);
|
|
21565
|
-
_pending.delete(requestId);
|
|
21566
|
-
_coalesce.delete(entry.coalesceKey);
|
|
21567
|
-
const safe =
|
|
21568
|
-
decision && typeof decision === "object" ? decision : { approve: false };
|
|
21569
|
-
entry.resolve(safe);
|
|
21570
|
-
for (const j of entry.joinedResolvers) j.resolve(safe);
|
|
21571
|
-
}
|
|
21572
|
-
|
|
21573
|
-
function _resetForTest() {
|
|
21574
|
-
for (const entry of _pending.values()) clearTimeout(entry.timeout);
|
|
21575
|
-
_pending.clear();
|
|
21576
|
-
_coalesce.clear();
|
|
21577
|
-
_idCounter = 0;
|
|
21578
|
-
}
|
|
21579
|
-
|
|
21580
|
-
let _handlersRegistered = false;
|
|
21581
|
-
/**
|
|
21582
|
-
* Wire the renderer→main response IPC. Idempotent.
|
|
21583
|
-
* Call once from main.js alongside other ipcMain setup.
|
|
21584
|
-
*/
|
|
21585
|
-
function setupJitConsentHandlers() {
|
|
21586
|
-
if (_handlersRegistered) return;
|
|
21587
|
-
if (!ipcMain$2 || typeof ipcMain$2.on !== "function") return;
|
|
21588
|
-
ipcMain$2.on(RESPONSE_CHANNEL, (_event, payload) => {
|
|
21589
|
-
_handleResponse(payload);
|
|
21590
|
-
});
|
|
21591
|
-
_handlersRegistered = true;
|
|
21592
|
-
}
|
|
21593
|
-
|
|
21594
|
-
var jitConsent$1 = {
|
|
21595
|
-
requestApproval: requestApproval$1,
|
|
21596
|
-
setupJitConsentHandlers,
|
|
21597
|
-
_handleResponse,
|
|
21598
|
-
_resetForTest,
|
|
21599
|
-
REQUEST_CHANNEL,
|
|
21600
|
-
RESPONSE_CHANNEL,
|
|
21601
|
-
DEFAULT_TIMEOUT_MS,
|
|
21602
|
-
};
|
|
21603
|
-
|
|
21604
22032
|
/**
|
|
21605
22033
|
* permissionGate.js
|
|
21606
22034
|
*
|
|
@@ -22081,37 +22509,6 @@ var mcpScopeResolver = {
|
|
|
22081
22509
|
applyPathScopeToCredentials: applyPathScopeToCredentials$1,
|
|
22082
22510
|
};
|
|
22083
22511
|
|
|
22084
|
-
/**
|
|
22085
|
-
* securityFlags.js
|
|
22086
|
-
*
|
|
22087
|
-
* Centralized readers for the two boolean security flags that gate the
|
|
22088
|
-
* MCP allowlist stack:
|
|
22089
|
-
* - security.enforceWidgetMcpPermissions
|
|
22090
|
-
* - security.enableJitConsent
|
|
22091
|
-
*
|
|
22092
|
-
* **Default semantics: ON.** A missing settings.json, a missing
|
|
22093
|
-
* `security` block, or an undefined field all yield `true`. Only an
|
|
22094
|
-
* explicit `false` opts out. This is intentional — the security stack
|
|
22095
|
-
* is on by default; users have to actively disable it. The
|
|
22096
|
-
* Privacy & Security panel surfaces the toggles + a confirm-on-disable
|
|
22097
|
-
* dialog so the disable path is deliberate.
|
|
22098
|
-
*
|
|
22099
|
-
* The readers are pure functions of a settings object so the
|
|
22100
|
-
* default-on semantics are pinned by unit tests without touching the
|
|
22101
|
-
* filesystem. The callers in mcpController.js wrap these with
|
|
22102
|
-
* settings.json IO.
|
|
22103
|
-
*/
|
|
22104
|
-
|
|
22105
|
-
function readEnforceFlag$1(settings) {
|
|
22106
|
-
return settings?.security?.enforceWidgetMcpPermissions !== false;
|
|
22107
|
-
}
|
|
22108
|
-
|
|
22109
|
-
function readJitFlag$1(settings) {
|
|
22110
|
-
return settings?.security?.enableJitConsent !== false;
|
|
22111
|
-
}
|
|
22112
|
-
|
|
22113
|
-
var securityFlags = { readEnforceFlag: readEnforceFlag$1, readJitFlag: readJitFlag$1 };
|
|
22114
|
-
|
|
22115
22512
|
/**
|
|
22116
22513
|
* mcpController.js
|
|
22117
22514
|
*
|
|
@@ -22491,7 +22888,7 @@ async function refreshGoogleOAuthToken(tokenRefresh) {
|
|
|
22491
22888
|
|
|
22492
22889
|
console.log("[mcpController] Refreshing Google OAuth token...");
|
|
22493
22890
|
|
|
22494
|
-
const https = require$$
|
|
22891
|
+
const https = require$$10;
|
|
22495
22892
|
const postData = [
|
|
22496
22893
|
`client_id=${encodeURIComponent(keyData.client_id)}`,
|
|
22497
22894
|
`client_secret=${encodeURIComponent(keyData.client_secret)}`,
|
|
@@ -48166,7 +48563,7 @@ var jsonSchemaToZod_1 = { jsonSchemaToZod: jsonSchemaToZod$1, jsonSchemaProperty
|
|
|
48166
48563
|
* - Rate limiting via token bucket (60 req/min)
|
|
48167
48564
|
*/
|
|
48168
48565
|
|
|
48169
|
-
const https$1 = require$$
|
|
48566
|
+
const https$1 = require$$10;
|
|
48170
48567
|
const { randomUUID } = require$$3$4;
|
|
48171
48568
|
const { BrowserWindow: BrowserWindow$1 } = require$$0$1;
|
|
48172
48569
|
const { McpServer } = mcp;
|
|
@@ -49975,7 +50372,7 @@ var themeFromUrlErrors$1 = {
|
|
|
49975
50372
|
|
|
49976
50373
|
const css = require$$0$8;
|
|
49977
50374
|
const { Vibrant } = require$$1$7;
|
|
49978
|
-
const https = require$$
|
|
50375
|
+
const https = require$$10;
|
|
49979
50376
|
const http = require$$0$7;
|
|
49980
50377
|
const { URL: URL$1 } = require$$4$1;
|
|
49981
50378
|
const {
|
|
@@ -62431,20 +62828,30 @@ const dataApi$2 = {
|
|
|
62431
62828
|
* @param {object} options { filename, extension }
|
|
62432
62829
|
* @param {object} returnEmpty the return empty object
|
|
62433
62830
|
*/
|
|
62434
|
-
saveData: (data, filename, append, returnEmpty,
|
|
62831
|
+
saveData: (data, filename, append, returnEmpty, widgetId = null) =>
|
|
62435
62832
|
ipcRenderer$n.invoke(DATA_SAVE_TO_FILE, {
|
|
62436
62833
|
data,
|
|
62437
62834
|
filename,
|
|
62438
62835
|
append,
|
|
62439
62836
|
returnEmpty,
|
|
62837
|
+
widgetId,
|
|
62440
62838
|
}),
|
|
62441
62839
|
|
|
62442
62840
|
/*
|
|
62443
62841
|
* readData
|
|
62444
62842
|
* @param {string} filename the filename to read (not path)
|
|
62843
|
+
* @param {string|null} widgetId Phase 2 JIT consent — the widget's
|
|
62844
|
+
* package name (e.g. "@trops/notes-summarizer"). Used by the fs
|
|
62845
|
+
* gate to scope per-widget read access. Null disables the gate
|
|
62846
|
+
* for legacy callers (`enforceWidgetMcpPermissions` flag still
|
|
62847
|
+
* gates the gate itself).
|
|
62445
62848
|
*/
|
|
62446
|
-
readData: (filename, returnEmpty = []) =>
|
|
62447
|
-
ipcRenderer$n.invoke(DATA_READ_FROM_FILE, {
|
|
62849
|
+
readData: (filename, returnEmpty = [], widgetId = null) =>
|
|
62850
|
+
ipcRenderer$n.invoke(DATA_READ_FROM_FILE, {
|
|
62851
|
+
filename,
|
|
62852
|
+
returnEmpty,
|
|
62853
|
+
widgetId,
|
|
62854
|
+
}),
|
|
62448
62855
|
|
|
62449
62856
|
/**
|
|
62450
62857
|
* transformFile
|