@trops/dash-core 0.1.523 → 0.1.525

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,214 @@ 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
+ scanWidgetPackagePermissions_1 = {
4004
+ scanFileForMcpUsage,
4005
+ scanWidgetPackagePermissions,
4006
+ mergePermissions,
4007
+ applyScanToPackageJson,
4008
+ };
4009
+ return scanWidgetPackagePermissions_1;
4010
+ }
4011
+
3804
4012
  /**
3805
4013
  * schedulerController.js
3806
4014
  *
@@ -4823,6 +5031,23 @@ var schedulerController_1 = schedulerController$2;
4823
5031
  throw new Error(`Unsupported local source type: ${resolvedPath}`);
4824
5032
  }
4825
5033
 
5034
+ // Slice 13a: scan the freshly-installed widget for MCP usage and
5035
+ // update the package.json's `dash.permissions.mcp` block. Always
5036
+ // runs (even when a manifest already exists) — catches author
5037
+ // forgetfulness and version updates that introduced new tools.
5038
+ // Merge is additive; hand-authored entries are preserved.
5039
+ try {
5040
+ const {
5041
+ applyScanToPackageJson,
5042
+ } = requireScanWidgetPackagePermissions();
5043
+ applyScanToPackageJson(widgetPath);
5044
+ } catch (e) {
5045
+ console.warn(
5046
+ `[WidgetRegistry] Permission scan failed for ${widgetName}: ${e.message}`,
5047
+ );
5048
+ // Non-fatal — the gate's runtime JIT still backs us up.
5049
+ }
5050
+
4826
5051
  let config = await this.loadWidgetConfig(widgetName, widgetPath);
4827
5052
 
4828
5053
  if (dashConfigPath) {
@@ -63166,6 +63391,30 @@ async function prepareWidgetForPublish$1(appId, packageId, options = {}) {
63166
63391
  const parsedName = parsePackageName(pkgJson.name || "");
63167
63392
  const resolvedScope = options.scope || callerScope;
63168
63393
 
63394
+ // Slice 13a: scan source for MCP usage and refresh the
63395
+ // `dash.permissions.mcp` block in package.json BEFORE the zip is
63396
+ // built. Always runs (even when a manifest exists) — catches
63397
+ // author forgetfulness and version updates that introduced new
63398
+ // tools. Merge is additive; hand-authored entries are preserved.
63399
+ try {
63400
+ const {
63401
+ applyScanToPackageJson,
63402
+ } = requireScanWidgetPackagePermissions();
63403
+ const merged = applyScanToPackageJson(widget.path);
63404
+ if (merged) {
63405
+ // Re-read pkgJson so subsequent steps in this function see the
63406
+ // updated manifest (the version bump below uses pkgJson too).
63407
+ if (fs.existsSync(pkgJsonPath)) {
63408
+ pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
63409
+ }
63410
+ }
63411
+ } catch (e) {
63412
+ console.warn(
63413
+ `[widgetRegistryController] Permission scan failed for ${packageId}: ${e.message}`,
63414
+ );
63415
+ // Non-fatal — runtime gate is still in place.
63416
+ }
63417
+
63169
63418
  // 3.5 Pre-zip privacy scan. Flag any personal filesystem paths baked
63170
63419
  // into shipped source (e.g. someone edited a `.dash.js`'s
63171
63420
  // `defaultValue` from `~/Library/...` to `/Users/me/...` to skip