@trops/dash-core 0.1.496 → 0.1.497

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.
@@ -1873,7 +1873,7 @@ var safePath_1 = {
1873
1873
  // it can mark the relative import as transitive-external and then fail
1874
1874
  // to resolve back. Deferring the require breaks the static-analysis
1875
1875
  // chain so the rollup build resolves cleanly.
1876
- const DEFAULT_TIMEOUT_MS = 1000;
1876
+ const DEFAULT_TIMEOUT_MS$1 = 1000;
1877
1877
  const DEFAULT_MEMORY_BYTES = 32 * 1024 * 1024; // 32 MB
1878
1878
 
1879
1879
  let _modulePromise = null;
@@ -1936,7 +1936,7 @@ async function runOnce({
1936
1936
  body,
1937
1937
  args = [],
1938
1938
  inputs = [],
1939
- timeoutMs = DEFAULT_TIMEOUT_MS,
1939
+ timeoutMs = DEFAULT_TIMEOUT_MS$1,
1940
1940
  memoryBytes = DEFAULT_MEMORY_BYTES,
1941
1941
  }) {
1942
1942
  if (typeof body !== "string" || !body.trim()) {
@@ -2007,7 +2007,7 @@ async function createCompiled({
2007
2007
  let disposed = false;
2008
2008
 
2009
2009
  return {
2010
- run(inputs, timeoutMs = DEFAULT_TIMEOUT_MS) {
2010
+ run(inputs, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
2011
2011
  if (disposed) {
2012
2012
  return { error: "executor already disposed" };
2013
2013
  }
@@ -2048,7 +2048,7 @@ async function createCompiled({
2048
2048
  var safeJsExecutor$1 = {
2049
2049
  runOnce,
2050
2050
  createCompiled,
2051
- DEFAULT_TIMEOUT_MS,
2051
+ DEFAULT_TIMEOUT_MS: DEFAULT_TIMEOUT_MS$1,
2052
2052
  DEFAULT_MEMORY_BYTES,
2053
2053
  };
2054
2054
 
@@ -21235,13 +21235,23 @@ function writeToDisk(data) {
21235
21235
  fs$b.renameSync(tmp, p);
21236
21236
  }
21237
21237
 
21238
- // Recognized origins for a persisted grant. "declared" means the user
21239
- // approved against the developer's declared dash.permissions.mcp block;
21240
- // "discovered" means the install-time scanner produced a synthetic
21241
- // manifest the user approved; "manual" means the user typed entries
21242
- // themselves in Settings → Privacy & Security with no manifest backing.
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.
21243
21248
  // Other values are dropped on persist (legacy grants stay null).
21244
- const ALLOWED_GRANT_ORIGINS = new Set(["declared", "discovered", "manual"]);
21249
+ const ALLOWED_GRANT_ORIGINS = new Set([
21250
+ "declared",
21251
+ "discovered",
21252
+ "manual",
21253
+ "live",
21254
+ ]);
21245
21255
 
21246
21256
  /**
21247
21257
  * Sanitize a perms object before persisting. Drops unknown keys, coerces
@@ -21289,7 +21299,7 @@ function getGrant$2(widgetId) {
21289
21299
  return all[widgetId] || null;
21290
21300
  }
21291
21301
 
21292
- function setGrant$1(widgetId, perms) {
21302
+ function setGrant$2(widgetId, perms) {
21293
21303
  if (typeof widgetId !== "string" || !widgetId) return false;
21294
21304
  const sanitized = sanitizePerms(perms);
21295
21305
  if (!sanitized) return false;
@@ -21371,7 +21381,7 @@ function clearCache$1() {
21371
21381
 
21372
21382
  var grantedPermissions = {
21373
21383
  getGrant: getGrant$2,
21374
- setGrant: setGrant$1,
21384
+ setGrant: setGrant$2,
21375
21385
  revokeGrant: revokeGrant$1,
21376
21386
  revokeServer: revokeServer$1,
21377
21387
  listAllGrants: listAllGrants$1,
@@ -21379,6 +21389,218 @@ var grantedPermissions = {
21379
21389
  ALLOWED_GRANT_ORIGINS,
21380
21390
  };
21381
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
+
21382
21604
  /**
21383
21605
  * permissionGate.js
21384
21606
  *
@@ -21418,8 +21640,9 @@ var grantedPermissions = {
21418
21640
  * dispatch goes through this gate.
21419
21641
  */
21420
21642
 
21421
- const { getGrant: getGrant$1 } = grantedPermissions;
21643
+ const { getGrant: getGrant$1, setGrant: setGrant$1 } = grantedPermissions;
21422
21644
  const { safePath: safePath$1 } = safePath_1;
21645
+ const { requestApproval } = jitConsent$1;
21423
21646
 
21424
21647
  // Argument keys that look like paths. Different MCP servers use
21425
21648
  // different conventions; this list covers the common filesystem-style
@@ -21535,8 +21758,148 @@ function gateToolCall$1({ widgetId, serverName, toolName, args }) {
21535
21758
  return { allow: true };
21536
21759
  }
21537
21760
 
21761
+ /**
21762
+ * Heuristic — "no grant" is the only denial reason JIT can recover.
21763
+ * Other denials (missing widgetId, malformed args, server-not-declared
21764
+ * post-grant, path-traversal-rejected) are bugs or attempted abuse,
21765
+ * not consent gaps; the user shouldn't be prompted about those.
21766
+ */
21767
+ function _isNoGrantDenial(reason) {
21768
+ return (
21769
+ typeof reason === "string" && /no MCP permissions granted/i.test(reason)
21770
+ );
21771
+ }
21772
+
21773
+ /**
21774
+ * Merge `addition` (a grant blob) into the widget's existing grant. Used
21775
+ * by the JIT path to extend an existing grant with a new tool/path
21776
+ * without clobbering grants for other servers.
21777
+ */
21778
+ function _mergeGrant(current, addition) {
21779
+ const out = {
21780
+ grantOrigin: addition.grantOrigin || current?.grantOrigin || null,
21781
+ servers: { ...(current?.servers || {}) },
21782
+ };
21783
+ for (const [name, perms] of Object.entries(addition.servers || {})) {
21784
+ const existing = out.servers[name] || {
21785
+ tools: [],
21786
+ readPaths: [],
21787
+ writePaths: [],
21788
+ };
21789
+ out.servers[name] = {
21790
+ tools: [...new Set([...(existing.tools || []), ...(perms.tools || [])])],
21791
+ readPaths: [
21792
+ ...new Set([...(existing.readPaths || []), ...(perms.readPaths || [])]),
21793
+ ],
21794
+ writePaths: [
21795
+ ...new Set([
21796
+ ...(existing.writePaths || []),
21797
+ ...(perms.writePaths || []),
21798
+ ]),
21799
+ ],
21800
+ };
21801
+ }
21802
+ return out;
21803
+ }
21804
+
21805
+ /**
21806
+ * Async wrapper around gateToolCall that escalates "no grant" denials
21807
+ * to a just-in-time consent prompt when `opts.enableJit` is true.
21808
+ * On approval, the user's chosen grant shape (carried on
21809
+ * `decision.granted`) is merged into the persisted grant and the gate
21810
+ * re-evaluates. On denial / timeout / disabled-flag, returns the
21811
+ * synchronous decision unchanged.
21812
+ *
21813
+ * @returns {Promise<{ allow: true } | { allow: false, reason: string }>}
21814
+ */
21815
+ async function gateToolCallWithJit$1(req, opts = {}) {
21816
+ const initial = gateToolCall$1(req);
21817
+ if (initial.allow) return initial;
21818
+ if (!opts.enableJit) return initial;
21819
+ if (!_isNoGrantDenial(initial.reason)) return initial;
21820
+
21821
+ let decision;
21822
+ try {
21823
+ decision = await requestApproval(
21824
+ {
21825
+ widgetId: req.widgetId,
21826
+ domain: "mcp",
21827
+ action: "callTool",
21828
+ args: {
21829
+ serverName: req.serverName,
21830
+ toolName: req.toolName,
21831
+ args: req.args || {},
21832
+ },
21833
+ },
21834
+ { timeoutMs: opts.timeoutMs },
21835
+ );
21836
+ } catch (e) {
21837
+ return {
21838
+ allow: false,
21839
+ reason:
21840
+ "JIT consent " +
21841
+ (e && e.message ? e.message : "failed") +
21842
+ "; original denial: " +
21843
+ initial.reason,
21844
+ };
21845
+ }
21846
+
21847
+ if (!decision || decision.approve !== true) {
21848
+ return {
21849
+ allow: false,
21850
+ reason:
21851
+ "user declined JIT consent for widget '" +
21852
+ req.widgetId +
21853
+ "' calling '" +
21854
+ req.toolName +
21855
+ "' on '" +
21856
+ req.serverName +
21857
+ "'",
21858
+ };
21859
+ }
21860
+
21861
+ // The renderer is expected to carry the chosen grant shape on
21862
+ // decision.granted. Fall back to a minimal tool-level grant if the
21863
+ // shape is missing — never silently grant paths the user didn't
21864
+ // explicitly approve.
21865
+ const addition =
21866
+ decision.granted && typeof decision.granted === "object"
21867
+ ? decision.granted
21868
+ : {
21869
+ grantOrigin: "live",
21870
+ servers: {
21871
+ [req.serverName]: {
21872
+ tools: [req.toolName],
21873
+ readPaths: [],
21874
+ writePaths: [],
21875
+ },
21876
+ },
21877
+ };
21878
+ // Force grantOrigin: "live" regardless of what the renderer sent.
21879
+ addition.grantOrigin = "live";
21880
+
21881
+ try {
21882
+ const current = getGrant$1(req.widgetId);
21883
+ const merged = _mergeGrant(current, addition);
21884
+ setGrant$1(req.widgetId, merged);
21885
+ } catch (e) {
21886
+ return {
21887
+ allow: false,
21888
+ reason:
21889
+ "JIT consent: failed to persist grant: " +
21890
+ (e && e.message ? e.message : String(e)),
21891
+ };
21892
+ }
21893
+
21894
+ // Re-evaluate against the freshly-persisted grant. If the user's
21895
+ // grant shape didn't actually cover the requested call (e.g. they
21896
+ // approved a different tool or path), the gate denies as usual.
21897
+ return gateToolCall$1(req);
21898
+ }
21899
+
21538
21900
  var permissionGate = {
21539
21901
  gateToolCall: gateToolCall$1,
21902
+ gateToolCallWithJit: gateToolCallWithJit$1,
21540
21903
  isWriteTool,
21541
21904
  PATH_ARG_KEYS,
21542
21905
  };
@@ -21742,7 +22105,7 @@ const path$e = require$$1$2;
21742
22105
  const fs$a = require$$0$2;
21743
22106
  const os$2 = require$$2$1;
21744
22107
  const responseCache$2 = responseCache_1;
21745
- const { gateToolCall } = permissionGate;
22108
+ const { gateToolCall, gateToolCallWithJit } = permissionGate;
21746
22109
  const { serverKey, parseServerKey } = mcpServerKey;
21747
22110
  const { applyPathScopeToCredentials } = mcpScopeResolver;
21748
22111
  const { app: app$7 } = require$$0$1;
@@ -21767,6 +22130,26 @@ function isWidgetPermissionEnforcementEnabled() {
21767
22130
  }
21768
22131
  }
21769
22132
 
22133
+ // Just-in-time consent feature flag (Phase 1 of the JIT consent slice).
22134
+ // When ON and the gate would deny for "no grant", we pause the call
22135
+ // and prompt the user via the JitConsentModal. Default OFF — the gate
22136
+ // fails closed as before until the user opts in.
22137
+ function isJitConsentEnabled() {
22138
+ try {
22139
+ const settingsPath = path$e.join(
22140
+ app$7.getPath("userData"),
22141
+ "Dashboard",
22142
+ "settings.json",
22143
+ );
22144
+ if (!fs$a.existsSync(settingsPath)) return false;
22145
+ const raw = fs$a.readFileSync(settingsPath, "utf8");
22146
+ const settings = JSON.parse(raw);
22147
+ return Boolean(settings?.security?.enableJitConsent);
22148
+ } catch (_e) {
22149
+ return false;
22150
+ }
22151
+ }
22152
+
21770
22153
  /**
21771
22154
  * Tool name prefixes considered safe to cache (read-only).
21772
22155
  * Writes/mutations are NOT cached so they always hit the source.
@@ -22530,16 +22913,19 @@ const mcpController$3 = {
22530
22913
 
22531
22914
  // Per-widget manifest gate. Activated by the
22532
22915
  // security.enforceWidgetMcpPermissions setting. When enabled
22533
- // and a widgetId is supplied, the widget's installed
22534
- // package.json's dash.permissions.mcp block determines what
22535
- // tools and paths are allowed.
22916
+ // and a widgetId is supplied, the widget's persisted grant
22917
+ // determines what tools and paths are allowed.
22918
+ //
22919
+ // JIT consent: when security.enableJitConsent is also on, the
22920
+ // gate escalates "no grant" denials into a runtime prompt
22921
+ // (jitConsent.requestApproval → renderer modal → grant write +
22922
+ // re-evaluate). Other denial reasons (path traversal, malformed
22923
+ // args, etc.) stay synchronous.
22536
22924
  if (isWidgetPermissionEnforcementEnabled() && widgetId) {
22537
- const gate = gateToolCall({
22538
- widgetId,
22539
- serverName,
22540
- toolName,
22541
- args,
22542
- });
22925
+ const gateReq = { widgetId, serverName, toolName, args };
22926
+ const gate = isJitConsentEnabled()
22927
+ ? await gateToolCallWithJit(gateReq, { enableJit: true })
22928
+ : gateToolCall(gateReq);
22543
22929
  if (!gate.allow) {
22544
22930
  throw new Error(`Widget permission gate: ${gate.reason}`);
22545
22931
  }
@@ -65573,6 +65959,7 @@ const webSocketController = webSocketController_1;
65573
65959
  const extractionCacheController = extractionCacheController_1;
65574
65960
  const mcpDashServerController = mcpDashServerController_1;
65575
65961
  const widgetMcpGrantsController = widgetMcpGrantsController$1;
65962
+ const jitConsent = jitConsent$1;
65576
65963
 
65577
65964
  // --- Errors ---
65578
65965
  const themeFromUrlErrors = themeFromUrlErrors$1;
@@ -65678,6 +66065,7 @@ var electron = {
65678
66065
  extractionCacheController,
65679
66066
  mcpDashServerController,
65680
66067
  widgetMcpGrantsController,
66068
+ jitConsent,
65681
66069
 
65682
66070
  // Controller functions (flat) — spread for convenient destructuring
65683
66071
  ...controllers,