@trops/dash-core 0.1.524 → 0.1.526

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.
@@ -3801,6 +3801,242 @@ var manifestScanner = {
3801
3801
  SCAN_FILE_LIMIT,
3802
3802
  };
3803
3803
 
3804
+ /**
3805
+ * scanWidgetPackagePermissions
3806
+ *
3807
+ * Walks a widget package directory, statically extracts MCP usage
3808
+ * (`useMcpProvider("type")` + `callTool("name", ...)`) from every
3809
+ * source file, and returns the canonical `dash.permissions.mcp` block
3810
+ * for embedding in the package's `package.json`.
3811
+ *
3812
+ * Used at three boundaries to keep declared permissions in sync with
3813
+ * the actual code:
3814
+ * - Widget publish flow (registry:publish-widget) — declarations
3815
+ * ship with the package.
3816
+ * - Widget install flow (installFromLocalPath) — declarations are
3817
+ * re-derived for already-published packages whose authors didn't
3818
+ * run the scanner, AND for new versions that updated their tool
3819
+ * set without updating the manifest.
3820
+ * - AI builder (widget:ai-build) — same scanner, single source of
3821
+ * truth.
3822
+ *
3823
+ * MERGE policy: scanner output is additive. Hand-authored entries in
3824
+ * `package.json.dash.permissions.mcp` are preserved; scanner-found
3825
+ * entries the human missed are appended. Idempotent — repeated runs
3826
+ * produce the same package.json (assuming source code unchanged).
3827
+ *
3828
+ * Limitations (acceptable, documented):
3829
+ * - Single-string regex scan. Variable-indirected calls like
3830
+ * `callTool(toolName, ...)` are skipped — runtime gate is the
3831
+ * safety net.
3832
+ * - Comments are stripped (line `//` only) so commented-out
3833
+ * examples don't pollute the declaration.
3834
+ * - Walks `widgets/`, `src/`, and the package root for `.js`,
3835
+ * `.jsx`, `.ts`, `.tsx` files. Skips `node_modules/` and `dist/`.
3836
+ */
3837
+
3838
+ var scanWidgetPackagePermissions_1;
3839
+ var hasRequiredScanWidgetPackagePermissions;
3840
+
3841
+ function requireScanWidgetPackagePermissions () {
3842
+ if (hasRequiredScanWidgetPackagePermissions) return scanWidgetPackagePermissions_1;
3843
+ hasRequiredScanWidgetPackagePermissions = 1;
3844
+
3845
+ const fs = require$$0$2;
3846
+ const path = require$$1$1;
3847
+
3848
+ const SOURCE_EXTS = new Set([".js", ".jsx", ".ts", ".tsx"]);
3849
+ const SKIP_DIRS = new Set([
3850
+ "node_modules",
3851
+ "dist",
3852
+ "build",
3853
+ ".git",
3854
+ "__tests__",
3855
+ "__mocks__",
3856
+ ]);
3857
+
3858
+ function _stripLineComments(code) {
3859
+ return code.replace(/\/\/[^\n]*/g, "");
3860
+ }
3861
+
3862
+ function _captureAll(code, pattern) {
3863
+ const out = [];
3864
+ for (const match of code.matchAll(pattern)) {
3865
+ out.push(match[1]);
3866
+ }
3867
+ return out;
3868
+ }
3869
+
3870
+ /**
3871
+ * Scan a single file's contents and return any detected MCP usage.
3872
+ * @param {string} code
3873
+ * @returns {{providers: string[], tools: string[]}}
3874
+ */
3875
+ function scanFileForMcpUsage(code) {
3876
+ if (typeof code !== "string" || !code) return { providers: [], tools: [] };
3877
+ const stripped = _stripLineComments(code);
3878
+ const providerPattern = /useMcpProvider\s*\(\s*["'`]([^"'`]+)["'`]/g;
3879
+ const callPattern = /callTool\s*\(\s*["'`]([^"'`]+)["'`]/g;
3880
+ return {
3881
+ providers: Array.from(new Set(_captureAll(stripped, providerPattern))),
3882
+ tools: Array.from(new Set(_captureAll(stripped, callPattern))),
3883
+ };
3884
+ }
3885
+
3886
+ function _walkSourceFiles(dir) {
3887
+ const out = [];
3888
+ if (!fs.existsSync(dir)) return out;
3889
+ let entries;
3890
+ try {
3891
+ entries = fs.readdirSync(dir, { withFileTypes: true });
3892
+ } catch (_) {
3893
+ return out;
3894
+ }
3895
+ for (const entry of entries) {
3896
+ if (entry.isDirectory()) {
3897
+ if (SKIP_DIRS.has(entry.name)) continue;
3898
+ out.push(..._walkSourceFiles(path.join(dir, entry.name)));
3899
+ } else if (entry.isFile()) {
3900
+ const ext = path.extname(entry.name);
3901
+ if (SOURCE_EXTS.has(ext)) out.push(path.join(dir, entry.name));
3902
+ }
3903
+ }
3904
+ return out;
3905
+ }
3906
+
3907
+ /**
3908
+ * Walk packageDir, scan every source file, return the canonical
3909
+ * `dash.permissions.mcp` block. Returns `{}` when no MCP usage found.
3910
+ */
3911
+ function scanWidgetPackagePermissions(packageDir) {
3912
+ if (typeof packageDir !== "string" || !fs.existsSync(packageDir)) return {};
3913
+
3914
+ const allProviders = new Set();
3915
+ const allTools = new Set();
3916
+ for (const filePath of _walkSourceFiles(packageDir)) {
3917
+ let code;
3918
+ try {
3919
+ code = fs.readFileSync(filePath, "utf8");
3920
+ } catch (_) {
3921
+ continue;
3922
+ }
3923
+ const { providers, tools } = scanFileForMcpUsage(code);
3924
+ for (const p of providers) allProviders.add(p);
3925
+ for (const t of tools) allTools.add(t);
3926
+ }
3927
+
3928
+ if (allProviders.size === 0 || allTools.size === 0) return {};
3929
+
3930
+ const out = {};
3931
+ for (const provider of allProviders) {
3932
+ out[provider] = { tools: Array.from(allTools).sort() };
3933
+ }
3934
+ return out;
3935
+ }
3936
+
3937
+ /**
3938
+ * Merge scanner output with any hand-authored block. Additive:
3939
+ * - Servers in `human` are kept as-is.
3940
+ * - Servers in `scanned` not in `human` are added.
3941
+ * - When the same server is in both, the union of `tools` arrays is
3942
+ * written, preserving any read/write paths the human declared.
3943
+ */
3944
+ function mergePermissions(human, scanned) {
3945
+ const out = {};
3946
+ // Start with human entries (preserves their shape including paths).
3947
+ if (human && typeof human === "object") {
3948
+ for (const [name, perms] of Object.entries(human)) {
3949
+ out[name] = { ...perms };
3950
+ if (Array.isArray(perms.tools)) out[name].tools = [...perms.tools];
3951
+ }
3952
+ }
3953
+ if (scanned && typeof scanned === "object") {
3954
+ for (const [name, perms] of Object.entries(scanned)) {
3955
+ if (!out[name]) {
3956
+ out[name] = { tools: [...(perms.tools || [])] };
3957
+ } else {
3958
+ const existing = new Set(out[name].tools || []);
3959
+ for (const t of perms.tools || []) existing.add(t);
3960
+ out[name].tools = Array.from(existing).sort();
3961
+ }
3962
+ }
3963
+ }
3964
+ return out;
3965
+ }
3966
+
3967
+ /**
3968
+ * Apply the scanner's findings to `package.json` in `packageDir` —
3969
+ * read, merge with any existing `dash.permissions.mcp` block, write
3970
+ * back. Returns the resulting block (or null if no package.json).
3971
+ */
3972
+ function applyScanToPackageJson(packageDir) {
3973
+ const pkgPath = path.join(packageDir, "package.json");
3974
+ let existing = {};
3975
+ if (fs.existsSync(pkgPath)) {
3976
+ try {
3977
+ existing = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
3978
+ } catch (_) {
3979
+ existing = {};
3980
+ }
3981
+ }
3982
+ const scanned = scanWidgetPackagePermissions(packageDir);
3983
+ const human = existing.dash?.permissions?.mcp || null;
3984
+ const merged = mergePermissions(human, scanned);
3985
+ if (Object.keys(merged).length === 0) {
3986
+ // Nothing to write. Don't churn the file.
3987
+ return null;
3988
+ }
3989
+ const next = {
3990
+ ...existing,
3991
+ dash: {
3992
+ ...(existing.dash || {}),
3993
+ permissions: {
3994
+ ...((existing.dash || {}).permissions || {}),
3995
+ mcp: merged,
3996
+ },
3997
+ },
3998
+ };
3999
+ fs.writeFileSync(pkgPath, JSON.stringify(next, null, 2), "utf8");
4000
+ return merged;
4001
+ }
4002
+
4003
+ /**
4004
+ * Run `applyScanToPackageJson` over a list of widget package paths.
4005
+ * Used at registry boot to backfill `dash.permissions.mcp` for widgets
4006
+ * installed before the publish/install scanner hooks existed (or that
4007
+ * have since updated their tool set without re-publishing). Idempotent
4008
+ * — additive merge means repeated boots produce no further writes.
4009
+ *
4010
+ * @param {string[]} packagePaths
4011
+ * @returns {{scanned: number, modified: number, errors: Array<{path: string, error: string}>}}
4012
+ */
4013
+ function backfillPackagePermissions(packagePaths) {
4014
+ const summary = { scanned: 0, modified: 0, errors: [] };
4015
+ if (!Array.isArray(packagePaths)) return summary;
4016
+ for (const pkgPath of packagePaths) {
4017
+ if (typeof pkgPath !== "string" || !pkgPath) continue;
4018
+ if (!fs.existsSync(pkgPath)) continue;
4019
+ summary.scanned += 1;
4020
+ try {
4021
+ const merged = applyScanToPackageJson(pkgPath);
4022
+ if (merged) summary.modified += 1;
4023
+ } catch (e) {
4024
+ summary.errors.push({ path: pkgPath, error: e.message });
4025
+ }
4026
+ }
4027
+ return summary;
4028
+ }
4029
+
4030
+ scanWidgetPackagePermissions_1 = {
4031
+ scanFileForMcpUsage,
4032
+ scanWidgetPackagePermissions,
4033
+ mergePermissions,
4034
+ applyScanToPackageJson,
4035
+ backfillPackagePermissions,
4036
+ };
4037
+ return scanWidgetPackagePermissions_1;
4038
+ }
4039
+
3804
4040
  /**
3805
4041
  * schedulerController.js
3806
4042
  *
@@ -4587,6 +4823,33 @@ var schedulerController_1 = schedulerController$2;
4587
4823
 
4588
4824
  // Reconcile: re-register orphaned widget packages found on disk
4589
4825
  this.reconcileWithDisk();
4826
+
4827
+ // Slice 13b: backfill `dash.permissions.mcp` for any registered
4828
+ // widget that was installed before the publish/install scanner
4829
+ // hooks existed (or that has since updated tools without
4830
+ // re-publishing). Additive merge — idempotent across boots.
4831
+ try {
4832
+ const {
4833
+ backfillPackagePermissions,
4834
+ } = requireScanWidgetPackagePermissions();
4835
+ const paths = Array.from(this.widgets.values())
4836
+ .map((w) => w.path)
4837
+ .filter(Boolean);
4838
+ const summary = backfillPackagePermissions(paths);
4839
+ if (summary.modified > 0) {
4840
+ console.log(
4841
+ `[WidgetRegistry] Backfilled MCP permissions for ${summary.modified}/${summary.scanned} widget(s)`,
4842
+ );
4843
+ }
4844
+ if (summary.errors.length) {
4845
+ console.warn(`[WidgetRegistry] MCP backfill errors:`, summary.errors);
4846
+ }
4847
+ } catch (e) {
4848
+ console.warn(
4849
+ "[WidgetRegistry] MCP permissions backfill failed:",
4850
+ e.message,
4851
+ );
4852
+ }
4590
4853
  }
4591
4854
 
4592
4855
  /**
@@ -4823,6 +5086,23 @@ var schedulerController_1 = schedulerController$2;
4823
5086
  throw new Error(`Unsupported local source type: ${resolvedPath}`);
4824
5087
  }
4825
5088
 
5089
+ // Slice 13a: scan the freshly-installed widget for MCP usage and
5090
+ // update the package.json's `dash.permissions.mcp` block. Always
5091
+ // runs (even when a manifest already exists) — catches author
5092
+ // forgetfulness and version updates that introduced new tools.
5093
+ // Merge is additive; hand-authored entries are preserved.
5094
+ try {
5095
+ const {
5096
+ applyScanToPackageJson,
5097
+ } = requireScanWidgetPackagePermissions();
5098
+ applyScanToPackageJson(widgetPath);
5099
+ } catch (e) {
5100
+ console.warn(
5101
+ `[WidgetRegistry] Permission scan failed for ${widgetName}: ${e.message}`,
5102
+ );
5103
+ // Non-fatal — the gate's runtime JIT still backs us up.
5104
+ }
5105
+
4826
5106
  let config = await this.loadWidgetConfig(widgetName, widgetPath);
4827
5107
 
4828
5108
  if (dashConfigPath) {
@@ -63166,6 +63446,30 @@ async function prepareWidgetForPublish$1(appId, packageId, options = {}) {
63166
63446
  const parsedName = parsePackageName(pkgJson.name || "");
63167
63447
  const resolvedScope = options.scope || callerScope;
63168
63448
 
63449
+ // Slice 13a: scan source for MCP usage and refresh the
63450
+ // `dash.permissions.mcp` block in package.json BEFORE the zip is
63451
+ // built. Always runs (even when a manifest exists) — catches
63452
+ // author forgetfulness and version updates that introduced new
63453
+ // tools. Merge is additive; hand-authored entries are preserved.
63454
+ try {
63455
+ const {
63456
+ applyScanToPackageJson,
63457
+ } = requireScanWidgetPackagePermissions();
63458
+ const merged = applyScanToPackageJson(widget.path);
63459
+ if (merged) {
63460
+ // Re-read pkgJson so subsequent steps in this function see the
63461
+ // updated manifest (the version bump below uses pkgJson too).
63462
+ if (fs.existsSync(pkgJsonPath)) {
63463
+ pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
63464
+ }
63465
+ }
63466
+ } catch (e) {
63467
+ console.warn(
63468
+ `[widgetRegistryController] Permission scan failed for ${packageId}: ${e.message}`,
63469
+ );
63470
+ // Non-fatal — runtime gate is still in place.
63471
+ }
63472
+
63169
63473
  // 3.5 Pre-zip privacy scan. Flag any personal filesystem paths baked
63170
63474
  // into shipped source (e.g. someone edited a `.dash.js`'s
63171
63475
  // `defaultValue` from `~/Library/...` to `/Users/me/...` to skip