@trops/dash-core 0.1.507 → 0.1.509

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.
@@ -993,14 +993,14 @@ var events$8 = {
993
993
  * Open a dialog window for choosing files
994
994
  */
995
995
 
996
- const { dialog: dialog$2 } = require$$0$1;
996
+ const { dialog: dialog$3 } = require$$0$1;
997
997
  const events$7 = events$8;
998
998
 
999
999
  const showDialog$1 = async (win, message, allowFile, extensions = ["*"]) => {
1000
1000
  const properties =
1001
1001
  allowFile === true ? ["openFile"] : ["openDirectory", "createDirectory"];
1002
1002
  const filters = allowFile === true ? [{ name: "Data", extensions }] : [];
1003
- const result = await dialog$2.showOpenDialog({ properties, filters });
1003
+ const result = await dialog$3.showOpenDialog({ properties, filters });
1004
1004
  if (result.canceled || !result.filePaths[0]) return null;
1005
1005
  return result.filePaths[0];
1006
1006
  };
@@ -2083,7 +2083,7 @@ var grantedPermissions = {
2083
2083
  * _resetForTest() → void (test-only)
2084
2084
  */
2085
2085
 
2086
- const { BrowserWindow: BrowserWindow$2, ipcMain: ipcMain$2 } = require$$0$1;
2086
+ const { BrowserWindow: BrowserWindow$3, ipcMain: ipcMain$2 } = require$$0$1;
2087
2087
 
2088
2088
  const REQUEST_CHANNEL = "widget:permission-required";
2089
2089
  const RESPONSE_CHANNEL = "widget:permission-response";
@@ -2129,7 +2129,7 @@ function coalesceKeyOf(req) {
2129
2129
  function emitEvent(payload) {
2130
2130
  let wins = [];
2131
2131
  try {
2132
- wins = BrowserWindow$2.getAllWindows() || [];
2132
+ wins = BrowserWindow$3.getAllWindows() || [];
2133
2133
  } catch {
2134
2134
  wins = [];
2135
2135
  }
@@ -2586,9 +2586,23 @@ function _hostMatches(host, allowedList) {
2586
2586
  if (!Array.isArray(allowedList) || allowedList.length === 0) return false;
2587
2587
  if (allowedList.includes("*")) return true;
2588
2588
  const lower = host.toLowerCase();
2589
- return allowedList.some(
2590
- (h) => typeof h === "string" && h.toLowerCase() === lower,
2591
- );
2589
+ for (const entry of allowedList) {
2590
+ if (typeof entry !== "string") continue;
2591
+ const entryLower = entry.toLowerCase();
2592
+ // Exact match.
2593
+ if (entryLower === lower) return true;
2594
+ // Subdomain wildcard: "*.example.com" matches "example.com" and
2595
+ // any host ending in ".example.com". The leading dot on the
2596
+ // suffix-test is required — otherwise "*.example.com" would also
2597
+ // match "attackerexample.com", which is the kind of confusion
2598
+ // this feature is meant to avoid.
2599
+ if (entryLower.startsWith("*.")) {
2600
+ const base = entryLower.slice(2); // "example.com"
2601
+ if (lower === base) return true;
2602
+ if (lower.endsWith("." + base)) return true;
2603
+ }
2604
+ }
2605
+ return false;
2592
2606
  }
2593
2607
 
2594
2608
  function _parseHost(url) {
@@ -48726,7 +48740,7 @@ var jsonSchemaToZod_1 = { jsonSchemaToZod: jsonSchemaToZod$1, jsonSchemaProperty
48726
48740
 
48727
48741
  const https$1 = require$$11;
48728
48742
  const { randomUUID } = require$$3$4;
48729
- const { BrowserWindow: BrowserWindow$1 } = require$$0$1;
48743
+ const { BrowserWindow: BrowserWindow$2 } = require$$0$1;
48730
48744
  const { McpServer } = mcp;
48731
48745
  const {
48732
48746
  StreamableHTTPServerTransport,
@@ -48770,7 +48784,7 @@ function broadcastStateChanged(toolName, result) {
48770
48784
  /* leave null */
48771
48785
  }
48772
48786
  const payload = { toolName, result: parsed };
48773
- for (const win of BrowserWindow$1.getAllWindows()) {
48787
+ for (const win of BrowserWindow$2.getAllWindows()) {
48774
48788
  if (!win.isDestroyed()) {
48775
48789
  try {
48776
48790
  win.webContents.send("dash-mcp:state-changed", payload);
@@ -57551,7 +57565,7 @@ function requireWidgetPublishManifest () {
57551
57565
  * and registry interaction.
57552
57566
  */
57553
57567
  const path$4 = require$$1$1;
57554
- const { app: app$4, dialog: dialog$1 } = require$$0$1;
57568
+ const { app: app$4, dialog: dialog$2 } = require$$0$1;
57555
57569
  const AdmZip$2 = require$$3$2;
57556
57570
 
57557
57571
  const themeController$3 = themeController_1;
@@ -57750,7 +57764,7 @@ async function prepareThemeForPublish$1(win, appId, themeKey, options = {}) {
57750
57764
  const sanitizedName = sanitizeName(themeKey);
57751
57765
  const defaultFilename = `theme-${sanitizedName}-v${manifest.version}.zip`;
57752
57766
 
57753
- const saveResult = await dialog$1.showSaveDialog(win, {
57767
+ const saveResult = await dialog$2.showSaveDialog(win, {
57754
57768
  title: "Save Theme Package",
57755
57769
  defaultPath: defaultFilename,
57756
57770
  filters: [{ name: "ZIP Files", extensions: ["zip"] }],
@@ -58289,7 +58303,7 @@ var themeRegistryController$1 = {
58289
58303
  * applies event wiring. (Import is implemented in DASH-13.)
58290
58304
  */
58291
58305
 
58292
- const { app: app$3, dialog } = require$$0$1;
58306
+ const { app: app$3, dialog: dialog$1 } = require$$0$1;
58293
58307
  const path$3 = require$$1$1;
58294
58308
  const AdmZip$1 = require$$3$2;
58295
58309
  const { getFileContents: getFileContents$1 } = file;
@@ -58451,7 +58465,7 @@ async function exportDashboardConfig$1(
58451
58465
  .replace(/\s+/g, "-")
58452
58466
  .toLowerCase();
58453
58467
 
58454
- const { canceled, filePath } = await dialog.showSaveDialog(win, {
58468
+ const { canceled, filePath } = await dialog$1.showSaveDialog(win, {
58455
58469
  title: "Export Dashboard as ZIP",
58456
58470
  defaultPath: path$3.join(
58457
58471
  app$3.getPath("desktop"),
@@ -58505,7 +58519,7 @@ async function exportDashboardConfig$1(
58505
58519
  */
58506
58520
  async function selectDashboardFile$1(win) {
58507
58521
  try {
58508
- const { canceled, filePaths } = await dialog.showOpenDialog(win, {
58522
+ const { canceled, filePaths } = await dialog$1.showOpenDialog(win, {
58509
58523
  title: "Import Dashboard Configuration",
58510
58524
  filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
58511
58525
  properties: ["openFile"],
@@ -58612,7 +58626,7 @@ async function importDashboardConfig$1(
58612
58626
  zipPath = options.filePath;
58613
58627
  } else {
58614
58628
  // Show file picker
58615
- const { canceled, filePaths } = await dialog.showOpenDialog(win, {
58629
+ const { canceled, filePaths } = await dialog$1.showOpenDialog(win, {
58616
58630
  title: "Import Dashboard Configuration",
58617
58631
  filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
58618
58632
  properties: ["openFile"],
@@ -59960,7 +59974,7 @@ async function prepareDashboardForPublish$1(
59960
59974
 
59961
59975
  // 9. Show save dialog for the publish package
59962
59976
  const sanitizedName = manifest.name;
59963
- const { canceled, filePath } = await dialog.showSaveDialog(win, {
59977
+ const { canceled, filePath } = await dialog$1.showSaveDialog(win, {
59964
59978
  title: "Save Dashboard Package for Registry",
59965
59979
  defaultPath: path$3.join(
59966
59980
  app$3.getPath("desktop"),
@@ -61458,6 +61472,133 @@ function buildGrantsListing$1(
61458
61472
 
61459
61473
  var widgetMcpGrantsListing = { buildGrantsListing: buildGrantsListing$1 };
61460
61474
 
61475
+ /**
61476
+ * grantDiff.js
61477
+ *
61478
+ * Pure-function diff between two grant blobs. Used by the
61479
+ * `widget-mcp:set-grant` IPC handler to decide whether the change
61480
+ * needs OS-native confirmation (broadening permissions) or can pass
61481
+ * through silently (revocations / equal / narrowing).
61482
+ *
61483
+ * Returns { broadening: boolean, summary: string[] }. `summary` is a
61484
+ * list of human-readable additions used to populate the native
61485
+ * confirm dialog.
61486
+ *
61487
+ * Broadening dimensions checked:
61488
+ * - servers: new server name present in newGrant.servers but not in
61489
+ * currentGrant.servers
61490
+ * - server tools: new tool name in an existing server's `tools[]`
61491
+ * - server paths: new entry in `readPaths[]` or `writePaths[]`
61492
+ * (including `*` wildcard added)
61493
+ * - domains.fs: new block, or new `readPaths[]` / `writePaths[]`
61494
+ * entry within an existing block (including `*`)
61495
+ * - domains.network: new block, or new `hosts[]` entry (including
61496
+ * `*` and `*.<base>` wildcards)
61497
+ *
61498
+ * Reductions, equality, and "no permission" → "no permission"
61499
+ * transitions are NOT broadening.
61500
+ */
61501
+
61502
+ function _arr(x) {
61503
+ return Array.isArray(x) ? x : [];
61504
+ }
61505
+
61506
+ function _added(currentList, newList) {
61507
+ const cur = new Set(_arr(currentList));
61508
+ const out = [];
61509
+ for (const item of _arr(newList)) {
61510
+ if (!cur.has(item)) out.push(item);
61511
+ }
61512
+ return out;
61513
+ }
61514
+
61515
+ function _diffServer(serverName, currentSrv, newSrv) {
61516
+ const summary = [];
61517
+ const cur = currentSrv || {};
61518
+ const nxt = newSrv || {};
61519
+
61520
+ for (const tool of _added(cur.tools, nxt.tools)) {
61521
+ summary.push(`server "${serverName}" tool "${tool}"`);
61522
+ }
61523
+ for (const p of _added(cur.readPaths, nxt.readPaths)) {
61524
+ summary.push(`server "${serverName}" readPath "${p}"`);
61525
+ }
61526
+ for (const p of _added(cur.writePaths, nxt.writePaths)) {
61527
+ summary.push(`server "${serverName}" writePath "${p}"`);
61528
+ }
61529
+ return summary;
61530
+ }
61531
+
61532
+ function _diffServers(curServers, nxtServers) {
61533
+ const summary = [];
61534
+ const cur = curServers || {};
61535
+ const nxt = nxtServers || {};
61536
+ for (const name of Object.keys(nxt)) {
61537
+ if (!cur[name]) {
61538
+ // Whole new server entry → list each component as broadening.
61539
+ const srv = nxt[name];
61540
+ const tools = _arr(srv?.tools);
61541
+ const reads = _arr(srv?.readPaths);
61542
+ const writes = _arr(srv?.writePaths);
61543
+ if (tools.length === 0 && reads.length === 0 && writes.length === 0) {
61544
+ // Empty-shell server entry — no actual permissions added.
61545
+ // Skip; not a meaningful broadening.
61546
+ continue;
61547
+ }
61548
+ summary.push(`new server "${name}"`);
61549
+ for (const t of tools) summary.push(` tool "${t}"`);
61550
+ for (const p of reads) summary.push(` readPath "${p}"`);
61551
+ for (const p of writes) summary.push(` writePath "${p}"`);
61552
+ } else {
61553
+ summary.push(..._diffServer(name, cur[name], nxt[name]));
61554
+ }
61555
+ }
61556
+ return summary;
61557
+ }
61558
+
61559
+ function _diffDomainsFs(curFs, nxtFs) {
61560
+ const summary = [];
61561
+ const cur = curFs || {};
61562
+ const nxt = nxtFs || {};
61563
+ for (const p of _added(cur.readPaths, nxt.readPaths)) {
61564
+ summary.push(`fs readPath "${p}"`);
61565
+ }
61566
+ for (const p of _added(cur.writePaths, nxt.writePaths)) {
61567
+ summary.push(`fs writePath "${p}"`);
61568
+ }
61569
+ return summary;
61570
+ }
61571
+
61572
+ function _diffDomainsNetwork(curNet, nxtNet) {
61573
+ const summary = [];
61574
+ const cur = curNet || {};
61575
+ const nxt = nxtNet || {};
61576
+ for (const h of _added(cur.hosts, nxt.hosts)) {
61577
+ summary.push(`network host "${h}"`);
61578
+ }
61579
+ return summary;
61580
+ }
61581
+
61582
+ /**
61583
+ * @param {object|null|undefined} currentGrant
61584
+ * @param {object|null|undefined} newGrant
61585
+ * @returns {{ broadening: boolean, summary: string[] }}
61586
+ */
61587
+ function isBroadening$1(currentGrant, newGrant) {
61588
+ const cur = currentGrant || {};
61589
+ const nxt = newGrant || {};
61590
+
61591
+ const summary = [
61592
+ ..._diffServers(cur.servers, nxt.servers),
61593
+ ..._diffDomainsFs(cur?.domains?.fs, nxt?.domains?.fs),
61594
+ ..._diffDomainsNetwork(cur?.domains?.network, nxt?.domains?.network),
61595
+ ];
61596
+
61597
+ return { broadening: summary.length > 0, summary };
61598
+ }
61599
+
61600
+ var grantDiff = { isBroadening: isBroadening$1 };
61601
+
61461
61602
  /**
61462
61603
  * widgetMcpGrantsController.js
61463
61604
  *
@@ -61473,7 +61614,7 @@ var widgetMcpGrantsListing = { buildGrantsListing: buildGrantsListing$1 };
61473
61614
  * grant are also surfaced — those are the install-consent retroactive prompts.
61474
61615
  */
61475
61616
 
61476
- const { ipcMain: ipcMain$1 } = require$$0$1;
61617
+ const { ipcMain: ipcMain$1, dialog, BrowserWindow: BrowserWindow$1 } = require$$0$1;
61477
61618
  const {
61478
61619
  getGrant,
61479
61620
  setGrant,
@@ -61484,13 +61625,62 @@ const {
61484
61625
  const { getWidgetMcpPermissions } = widgetPermissions;
61485
61626
  const { getWidgetRegistry } = widgetRegistryExports;
61486
61627
  const { buildGrantsListing } = widgetMcpGrantsListing;
61628
+ const { isBroadening } = grantDiff;
61629
+
61630
+ // Native confirm dialog for any set-grant call that broadens the
61631
+ // widget's current permissions. The dialog runs at OS level — a
61632
+ // renderer (including a malicious widget) cannot dismiss it
61633
+ // programmatically. This is the defense-in-depth fix for the
61634
+ // `widget-mcp:set-grant` consent-bypass gap documented in the IPC
61635
+ // audit doc: a widget calling `mainApi.widgetMcp.setGrant("@self",
61636
+ // {wide-open perms})` now triggers a system-level prompt the user
61637
+ // must explicitly approve. Reductions / equality pass unprompted.
61638
+ async function _confirmBroadening(event, widgetId, summary) {
61639
+ const senderWindow =
61640
+ BrowserWindow$1.fromWebContents(event.sender) ||
61641
+ BrowserWindow$1.getFocusedWindow();
61642
+ // Cap the listed lines so the dialog body stays readable.
61643
+ const MAX_LINES = 20;
61644
+ const trimmed = summary.slice(0, MAX_LINES);
61645
+ const overflow =
61646
+ summary.length > MAX_LINES
61647
+ ? `\n …and ${summary.length - MAX_LINES} more`
61648
+ : "";
61649
+ const detail =
61650
+ "Widget '" +
61651
+ widgetId +
61652
+ "' will be granted the following NEW permissions:\n\n " +
61653
+ trimmed.join("\n ") +
61654
+ overflow +
61655
+ "\n\nIf you didn't initiate this from Settings → Privacy & Security, " +
61656
+ "click Cancel — a malicious widget may be trying to escalate its own " +
61657
+ "permissions.";
61658
+
61659
+ const result = await dialog.showMessageBox(senderWindow, {
61660
+ type: "warning",
61661
+ title: "Confirm permissions change",
61662
+ message: "Allow new permissions for " + widgetId + "?",
61663
+ detail,
61664
+ buttons: ["Cancel", "Allow"],
61665
+ defaultId: 0,
61666
+ cancelId: 0,
61667
+ noLink: true,
61668
+ });
61669
+ return result.response === 1;
61670
+ }
61487
61671
 
61488
61672
  function setupWidgetMcpGrantsHandlers() {
61489
61673
  ipcMain$1.handle("widget-mcp:get-grant", (event, widgetId) => {
61490
61674
  return getGrant(widgetId);
61491
61675
  });
61492
61676
 
61493
- ipcMain$1.handle("widget-mcp:set-grant", (event, widgetId, perms) => {
61677
+ ipcMain$1.handle("widget-mcp:set-grant", async (event, widgetId, perms) => {
61678
+ const current = getGrant(widgetId);
61679
+ const diff = isBroadening(current, perms);
61680
+ if (diff.broadening) {
61681
+ const approved = await _confirmBroadening(event, widgetId, diff.summary);
61682
+ if (!approved) return false;
61683
+ }
61494
61684
  return setGrant(widgetId, perms);
61495
61685
  });
61496
61686