From 78e37fb986f7a96a9ae367201161c3f2ecf27e49 Mon Sep 17 00:00:00 2001 From: musab1258 Date: Sun, 28 Jun 2026 06:39:09 +0100 Subject: [PATCH] feat: create plugin system for third-party extensions (#411) --- docs/plugins/README.md | 77 ++ .../examples/activity-radar.manifest.json | 29 + package-lock.json | 611 +++++++++++--- package.json | 3 +- scripts/create-plugin.mjs | 197 +++++ .../dashboard/PluginRegistryView.tsx | 473 ++++++++++- src/components/dashboard/Settings.tsx | 8 + src/plugins/PluginManager.js | 789 +++++++++++++++--- src/plugins/__tests__/PluginManager.test.js | 85 ++ src/plugins/index.js | 93 +-- src/plugins/pluginCatalog.js | 220 +++++ src/plugins/pluginSandbox.jsx | 84 ++ src/plugins/pluginStorage.js | 127 +++ src/plugins/runtimeStatusPlugin.jsx | 2 + src/plugins/sdk.ts | 125 +++ tests/setup.js | 15 + 16 files changed, 2626 insertions(+), 312 deletions(-) create mode 100644 docs/plugins/README.md create mode 100644 docs/plugins/examples/activity-radar.manifest.json create mode 100644 scripts/create-plugin.mjs create mode 100644 src/plugins/__tests__/PluginManager.test.js create mode 100644 src/plugins/pluginCatalog.js create mode 100644 src/plugins/pluginSandbox.jsx create mode 100644 src/plugins/pluginStorage.js create mode 100644 src/plugins/sdk.ts diff --git a/docs/plugins/README.md b/docs/plugins/README.md new file mode 100644 index 00000000..8772380e --- /dev/null +++ b/docs/plugins/README.md @@ -0,0 +1,77 @@ +# Plugin System + +Stellar Dev Dashboard supports third-party extensions through a manifest-first plugin system. + +## Architecture + +- `module` plugins are trusted bundles that export a plugin factory and can provide React widgets and data sources. +- `iframe` plugins are sandboxed extensions. They run inside an isolated frame and only receive the permissions granted during install. +- Plugin records are persisted in browser storage so installed extensions survive reloads. + +## Manifest Format + +```json +{ + "id": "community.activity-radar", + "name": "Activity Radar", + "version": "1.2.0", + "description": "Sandboxed dashboard extension", + "author": { "name": "Community Labs" }, + "permissions": ["dashboard:read", "data:read"], + "runtime": { + "mode": "iframe", + "srcDoc": "...", + "sandbox": ["allow-scripts"] + }, + "widgets": [ + { + "id": "community.activity-radar.settings", + "title": "Activity Radar", + "placement": "settings", + "kind": "iframe" + } + ], + "dataSources": [] +} +``` + +## Permission Scopes + +- `dashboard:read` - read dashboard state and subscribe to state changes +- `dashboard:write` - use approved host actions such as navigation and search filters +- `data:read` - expose read-only data sources +- `data:write` - reserved for future writable plugin APIs +- `notifications:write` - create and remove notifications +- `network:request` - reserved for future remote fetch helpers +- `storage:read` - reserved for future isolated plugin storage access +- `storage:write` - reserved for future isolated plugin storage access +- `window:open` - reserved for future external link helpers + +## Install Flow + +1. The marketplace lists the manifest and requested permissions. +2. The user reviews the permissions in the plugin sidebar. +3. If approved, the manifest is persisted and the plugin is activated. +4. Marketplace updates can be compared by version and reinstalled when compatible. + +## SDK + +The public TypeScript SDK lives in `src/plugins/sdk.ts`. + +It exports: + +- `PLUGIN_PERMISSION_SCOPES` +- `definePlugin()` +- `createPluginManifest()` +- `createIframeWidget()` +- `comparePluginVersions()` + +## CLI + +Use the scaffold script to create a new plugin package: + +```bash +node scripts/create-plugin.mjs --name "My Plugin" --id "community.my-plugin" --output ./my-plugin +``` + +Add `--runtime module` if you want to generate a module-style starter instead of an iframe starter. diff --git a/docs/plugins/examples/activity-radar.manifest.json b/docs/plugins/examples/activity-radar.manifest.json new file mode 100644 index 00000000..febd1f4f --- /dev/null +++ b/docs/plugins/examples/activity-radar.manifest.json @@ -0,0 +1,29 @@ +{ + "id": "community.activity-radar", + "name": "Activity Radar", + "version": "1.2.0", + "description": "Surfaces recent dashboard activity in a sandboxed extension frame.", + "author": { + "name": "Community Labs" + }, + "permissions": [ + "dashboard:read", + "data:read" + ], + "runtime": { + "mode": "iframe", + "sandbox": [ + "allow-scripts" + ], + "srcDoc": "

Activity Radar

" + }, + "widgets": [ + { + "id": "community.activity-radar.settings", + "title": "Activity Radar", + "placement": "settings", + "kind": "iframe" + } + ], + "dataSources": [] +} diff --git a/package-lock.json b/package-lock.json index 5b612480..1263d1f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@axe-core/react": "^4.11.3", "@stellar/stellar-sdk": "^12.3.0", "@tensorflow/tfjs": "^4.22.0", - "date-fns": "^3.6.0", + "@tensorflow/tfjs-node": "^4.22.0", "d3-array": "^3.2.4", "d3-force-3d": "^3.0.6", "d3-scale": "^4.0.2", @@ -20,6 +20,8 @@ "d3-shape": "^3.2.0", "d3-time-format": "^4.1.0", "d3-zoom": "^3.0.0", + "date-fns": "^3.6.0", + "express": "^4.18.2", "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", "idb": "^8.0.3", @@ -31,6 +33,7 @@ "react-i18next": "^15.0.2", "react-router-dom": "^6.30.4", "recharts": "^2.12.7", + "swr": "^2.2.0", "uuid": "^9.0.1", "zustand": "^4.5.4" }, @@ -4848,6 +4851,62 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", + "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.41.5", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.5.tgz", @@ -5539,6 +5598,9 @@ "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", "license": "Apache-2.0" }, + "node_modules/@stellar/ledger": { + "optional": true + }, "node_modules/@stellar/stellar-base": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-12.1.1.tgz", @@ -6541,6 +6603,116 @@ "@tensorflow/tfjs-core": "4.22.0" } }, + "node_modules/@tensorflow/tfjs-node": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-node/-/tfjs-node-4.22.0.tgz", + "integrity": "sha512-uHrXeUlfgkMxTZqHkESSV7zSdKdV0LlsBeblqkuKU9nnfxB1pC6DtoyYVaLxznzZy7WQSegjcohxxCjAf6Dc7w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@mapbox/node-pre-gyp": "1.0.9", + "@tensorflow/tfjs": "4.22.0", + "adm-zip": "^0.5.2", + "google-protobuf": "^3.9.2", + "https-proxy-agent": "^2.2.1", + "progress": "^2.0.0", + "rimraf": "^2.6.2", + "tar": "^6.2.1" + }, + "engines": { + "node": ">=8.11.0" + } + }, + "node_modules/@tensorflow/tfjs-node/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "license": "MIT", + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@tensorflow/tfjs-node/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@tensorflow/tfjs-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@tensorflow/tfjs-node/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tensorflow/tfjs-node/node_modules/https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "license": "MIT", + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/@tensorflow/tfjs-node/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@tensorflow/tfjs-node/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/@tensorflow/tfjs/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -6687,6 +6859,13 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tweenjs/tween.js": { "version": "25.0.0", "resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", @@ -7445,11 +7624,16 @@ "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", "license": "BSD-3-Clause" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -7463,7 +7647,15 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/accessor-fn": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", "license": "MIT", "engines": { "node": ">=12" @@ -7492,11 +7684,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "4" @@ -7585,6 +7785,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -7623,7 +7843,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, "license": "MIT" }, "node_modules/array-includes": { @@ -7882,7 +8101,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/bare-addon-resolve": { @@ -8125,7 +8343,6 @@ "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -8150,7 +8367,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -8160,7 +8376,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -8264,7 +8479,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8430,6 +8644,15 @@ "node": ">= 16" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/chrome-launcher": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz", @@ -8627,6 +8850,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -8702,7 +8934,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/configstore": { @@ -8739,11 +8970,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -8756,7 +8992,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8787,7 +9022,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, "license": "MIT" }, "node_modules/core-js": { @@ -9111,6 +9345,16 @@ "node": ">=12" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -9193,7 +9437,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -9329,11 +9572,16 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9343,7 +9591,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9364,13 +9611,21 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1312386", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", @@ -9462,7 +9717,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -9482,7 +9736,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9718,6 +9971,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -9783,7 +10051,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -10151,7 +10418,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10250,7 +10516,6 @@ "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -10297,7 +10562,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10307,7 +10571,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -10317,14 +10580,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/express/node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "dev": true, "license": "MIT" }, "node_modules/external-editor": { @@ -10567,7 +10828,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -10586,7 +10846,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -10596,7 +10855,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/find-up": { @@ -10749,7 +11007,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10759,17 +11016,45 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -10827,6 +11112,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -11021,6 +11333,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -11127,6 +11445,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -11200,7 +11524,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -11255,7 +11578,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "6", @@ -11311,7 +11633,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -11400,12 +11721,20 @@ "node": ">=8" } }, + "node_modules/index-array-by": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/index-array-by/-/index-array-by-1.4.2.tgz", + "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -11688,20 +12017,12 @@ "node": ">=4" } }, - "node_modules/index-array-by": { - "version": "1.4.2", - "resolved": "https://registry.npmmirror.com/index-array-by/-/index-array-by-1.4.2.tgz", - "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "node_modules/inquirer/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -11754,7 +12075,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -13458,7 +13778,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13478,7 +13797,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -13495,7 +13813,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13532,7 +13849,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -13638,6 +13954,37 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -13672,7 +14019,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msw": { @@ -13823,7 +14169,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -13844,21 +14189,18 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, "license": "MIT" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -13872,6 +14214,21 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -13902,6 +14259,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -13915,7 +14285,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14013,7 +14382,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -14036,7 +14404,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -14285,7 +14652,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -14305,7 +14671,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14589,7 +14954,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -14616,7 +14980,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -14728,7 +15091,6 @@ "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -14753,7 +15115,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -14763,7 +15124,6 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -14982,6 +15342,20 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -15266,7 +15640,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -15282,7 +15655,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -15294,7 +15666,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -15315,7 +15686,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -15519,7 +15889,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/saxes": { @@ -15564,7 +15933,6 @@ "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -15589,7 +15957,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -15599,14 +15966,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -15622,7 +15987,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, "license": "ISC" }, "node_modules/set-cookie-parser": { @@ -15684,7 +16048,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, "license": "ISC" }, "node_modules/sha.js": { @@ -15734,7 +16097,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -15754,7 +16116,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -15771,7 +16132,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -15790,7 +16150,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -16023,7 +16382,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -16343,6 +16701,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", + "integrity": "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -16363,6 +16734,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", @@ -16391,6 +16780,33 @@ "streamx": "^2.15.0" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -16696,7 +17112,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -16848,7 +17263,6 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -17138,7 +17552,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -17245,11 +17658,16 @@ "which-typed-array": "^1.1.2" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -17273,7 +17691,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -17663,6 +18080,15 @@ "node": ">=8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17713,7 +18139,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index 7289b9ab..4cc6071f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "preview": "vite preview", "lint": "eslint .", "lint:fix": "eslint . --fix", + "plugin:create": "node scripts/create-plugin.mjs", "format": "prettier --write package.json package-lock.json eslint.config.js tsconfig.json .github/workflows/ci.yml .prettierrc.json", "format:check": "prettier --check package.json package-lock.json eslint.config.js tsconfig.json .github/workflows/ci.yml .prettierrc.json", "type-check": "tsc --noEmit", @@ -40,7 +41,7 @@ "docs:api:generate": "node scripts/generate-api-docs.mjs" }, "dependencies": { - "@tensorflow/tfjs-node": "^5.0.0", + "@tensorflow/tfjs-node": "^4.22.0", "express": "^4.18.2", "@axe-core/react": "^4.11.3", "@stellar/stellar-sdk": "^12.3.0", diff --git a/scripts/create-plugin.mjs b/scripts/create-plugin.mjs new file mode 100644 index 00000000..1298957d --- /dev/null +++ b/scripts/create-plugin.mjs @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; + +function parseArgs(argv) { + const args = { + name: "", + id: "", + output: "", + runtime: "iframe", + }; + + for (let index = 2; index < argv.length; index += 1) { + const value = argv[index]; + const next = argv[index + 1]; + + if (value === "--name" && next) { + args.name = next; + index += 1; + continue; + } + + if (value === "--id" && next) { + args.id = next; + index += 1; + continue; + } + + if (value === "--output" && next) { + args.output = next; + index += 1; + continue; + } + + if (value === "--runtime" && next) { + args.runtime = next === "module" ? "module" : "iframe"; + index += 1; + } + } + + return args; +} + +function buildManifest({ id, name, runtime }) { + return { + id, + name, + version: "0.1.0", + description: `${name} plugin scaffold generated by the Stellar Dev Dashboard CLI.`, + author: { + name: "Your name", + }, + permissions: ["dashboard:read"], + runtime: + runtime === "module" + ? { + mode: "module", + entry: "./src/plugin.ts", + } + : { + mode: "iframe", + sandbox: ["allow-scripts"], + srcDoc: "
", + }, + widgets: [ + { + id: `${id}.settings`, + title: name, + placement: "settings", + kind: runtime === "module" ? "react" : "iframe", + }, + ], + dataSources: [], + }; +} + +function buildReadme({ name, id, runtime }) { + return `# ${name} + +Scaffolded plugin: \`${id}\` + +## Runtime + +\`${runtime}\` + +## Next steps + +1. Update the manifest and permission scopes. +2. Implement the widget UI. +3. Wire the plugin into your local extension host or static iframe server. +`; +} + +function buildIframeHtml({ name, id }) { + return ` + + + + + ${name} + + + +
+

${name}

+

This scaffold starts as a sandboxed iframe plugin. Replace this content with your own UI.

+ ${id} +
+ +`; +} + +async function main() { + const args = parseArgs(process.argv); + + if (!args.name || !args.id || !args.output) { + console.error( + "Usage: node scripts/create-plugin.mjs --name \"My Plugin\" --id \"community.my-plugin\" --output ./my-plugin [--runtime iframe|module]" + ); + process.exit(1); + } + + const targetDir = path.resolve(process.cwd(), args.output); + const srcDir = path.join(targetDir, "src"); + + await fs.mkdir(srcDir, { recursive: true }); + + const manifest = buildManifest(args); + await fs.writeFile(path.join(targetDir, "plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`); + await fs.writeFile(path.join(targetDir, "README.md"), `${buildReadme(args)}\n`); + await fs.writeFile( + path.join(srcDir, "widget.html"), + `${buildIframeHtml(args)}\n` + ); + + await fs.writeFile( + path.join(targetDir, "package.json"), + `${JSON.stringify( + { + name: args.id, + private: true, + version: "0.1.0", + }, + null, + 2 + )}\n` + ); + + console.log(`Created plugin scaffold in ${targetDir}`); +} + +main().catch((error) => { + console.error(error?.message || String(error)); + process.exit(1); +}); diff --git a/src/components/dashboard/PluginRegistryView.tsx b/src/components/dashboard/PluginRegistryView.tsx index d11d675b..9f4ed7ba 100644 --- a/src/components/dashboard/PluginRegistryView.tsx +++ b/src/components/dashboard/PluginRegistryView.tsx @@ -3,36 +3,49 @@ import { pluginManager, registerActivePlugins } from "../../plugins"; import { PLUGIN_STATUSES } from "../../plugins/PluginManager"; interface PluginWidget { - id: string - component: React.ComponentType> - pluginName: string - title: string - pluginId: string - props?: Record + id: string; + component: React.ComponentType>; + pluginName: string; + title: string; + pluginId: string; + props?: Record; } interface PluginRecord { - id: string - name: string - status: string - error?: string + id: string; + name: string; + status: string; + error?: string; + version?: string; + latestVersion?: string; + updateAvailable?: boolean; + enabled?: boolean; + sourceType?: string; + permissionsGranted?: string[]; + permissionsRequested?: string[]; + dependencies?: string[]; + installedAt?: string | null; } -interface PluginSnapshot { - plugins: PluginRecord[] - widgets: PluginWidget[] - dataSources: unknown[] -} - -interface PluginWidgetFrameProps { - widget: PluginWidget +interface MarketplacePlugin extends PluginRecord { + description?: string; + author?: string | { name: string }; + homepageUrl?: string | null; + runtime?: { mode?: string }; + widgets?: Array>; + dataSources?: Array>; + installed?: boolean; + installedVersion?: string | null; } -interface PluginStatusPillProps { - status: string +interface PluginSnapshot { + plugins: PluginRecord[]; + widgets: PluginWidget[]; + dataSources: unknown[]; + marketplace: MarketplacePlugin[]; } -function PluginWidgetFrame({ widget }: PluginWidgetFrameProps) { +function PluginWidgetFrame({ widget }: { widget: PluginWidget }) { const Component = widget.component; return ( @@ -65,11 +78,14 @@ function PluginWidgetFrame({ widget }: PluginWidgetFrameProps) { ); } -function PluginStatusPill({ status }: PluginStatusPillProps) { +function PluginStatusPill({ status }: { status: string }) { const colorByStatus: Record = { [PLUGIN_STATUSES.INITIALIZED]: "var(--green)", [PLUGIN_STATUSES.REGISTERED]: "var(--cyan)", [PLUGIN_STATUSES.FAILED]: "var(--red)", + [PLUGIN_STATUSES.BLOCKED]: "var(--amber)", + [PLUGIN_STATUSES.PENDING_REVIEW]: "var(--amber)", + [PLUGIN_STATUSES.DISABLED]: "var(--text-muted)", }; return ( @@ -88,41 +104,282 @@ function PluginStatusPill({ status }: PluginStatusPillProps) { ); } +function PermissionChip({ permission }: { permission: string }) { + return ( + + {permission} + + ); +} + +function MarketplaceCard({ + plugin, + onReview, + onUpdate, + onToggleEnabled, + onUninstall, + busy, +}: { + plugin: MarketplacePlugin; + onReview: (_plugin: MarketplacePlugin) => void; + onUpdate: (_pluginId: string) => void; + onToggleEnabled: (_pluginId: string, _nextEnabled: boolean) => void; + onUninstall: (_pluginId: string) => void; + busy: boolean; +}) { + const isInstalled = Boolean(plugin.installed); + const actionLabel = isInstalled ? (plugin.updateAvailable ? "Update" : "Details") : "Review"; + const permissions = Array.isArray(plugin.permissionsRequested) ? plugin.permissionsRequested : []; + + return ( +
+
+
+
+ {typeof plugin.author === "string" ? plugin.author : plugin.author?.name || "Community"} +
+
+ {plugin.name} +
+
+ {plugin.description} +
+
+
+ + + v{plugin.version} + {plugin.updateAvailable && plugin.latestVersion ? ` → v${plugin.latestVersion}` : ""} + +
+
+ +
+ {permissions.length > 0 ? permissions.map((permission) => ( + + )) : ( + No permissions requested + )} +
+ +
+ + + {isInstalled && ( + <> + + + + )} +
+
+ ); +} + +function SummaryStat({ label, value }: { label: string; value: string | number }) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} + export default function PluginRegistryView({ placement = "settings" }: { placement?: string }) { const [snapshot, setSnapshot] = useState(() => ({ plugins: pluginManager.getPluginRecords(), widgets: pluginManager.getWidgets({ placement }), dataSources: pluginManager.getDataSources(), + marketplace: [], })); + const [selectedPlugin, setSelectedPlugin] = useState(null); + const [busyPluginId, setBusyPluginId] = useState(null); + const [error, setError] = useState(null); useEffect(() => { - const refresh = () => { - setSnapshot({ - plugins: pluginManager.getPluginRecords(), - widgets: pluginManager.getWidgets({ placement }), - dataSources: pluginManager.getDataSources(), - }); + let cancelled = false; + + const refresh = async () => { + try { + const [marketplace, plugins, widgets, dataSources] = await Promise.all([ + pluginManager.getMarketplacePlugins(), + Promise.resolve(pluginManager.getPluginRecords()), + Promise.resolve(pluginManager.getWidgets({ placement })), + Promise.resolve(pluginManager.getDataSources()), + ]); + + if (!cancelled) { + setSnapshot({ marketplace, plugins, widgets, dataSources }); + } + } catch (refreshError) { + if (!cancelled) { + setError(refreshError?.message || String(refreshError)); + } + } }; refresh(); - return pluginManager.subscribe(refresh); + const unsubscribe = pluginManager.subscribe(refresh); + return () => { + cancelled = true; + unsubscribe(); + }; }, [placement]); useEffect(() => { - registerActivePlugins().catch((error: Error) => { - console.error("Plugin registration failed", error); + registerActivePlugins().catch((pluginError) => { + setError(pluginError?.message || String(pluginError)); }); }, []); const pluginCount = snapshot.plugins.length; const dataSourceCount = snapshot.dataSources.length; + const installedCount = snapshot.marketplace.filter((plugin) => plugin.installed).length; + const updateCount = snapshot.marketplace.filter((plugin) => plugin.updateAvailable).length; + const widgets = useMemo(() => snapshot.widgets, [snapshot.widgets]); + const handleInstall = async (plugin: MarketplacePlugin) => { + setBusyPluginId(plugin.id); + setError(null); + try { + await pluginManager.installPlugin(plugin, { + approvedPermissions: plugin.permissionsRequested || [], + }); + setSelectedPlugin(null); + } catch (installError) { + setError(installError?.message || String(installError)); + } finally { + setBusyPluginId(null); + } + }; + + const handleUpdate = async (pluginId: string) => { + setBusyPluginId(pluginId); + setError(null); + try { + await pluginManager.updatePlugin(pluginId); + } catch (updateError) { + setError(updateError?.message || String(updateError)); + } finally { + setBusyPluginId(null); + } + }; + + const handleToggleEnabled = async (pluginId: string, nextEnabled: boolean) => { + setBusyPluginId(pluginId); + setError(null); + try { + await pluginManager.setPluginEnabled(pluginId, nextEnabled); + } catch (toggleError) { + setError(toggleError?.message || String(toggleError)); + } finally { + setBusyPluginId(null); + } + }; + + const handleRemove = async (pluginId: string) => { + setBusyPluginId(pluginId); + setError(null); + try { + await pluginManager.uninstallPlugin(pluginId); + if (selectedPlugin?.id === pluginId) { + setSelectedPlugin(null); + } + } catch (removeError) { + setError(removeError?.message || String(removeError)); + } finally { + setBusyPluginId(null); + } + }; + return (
-
- {pluginCount} plugins +
+ {pluginCount} installed {widgets.length} widgets {dataSourceCount} data sources
+
+ + plugin.status === PLUGIN_STATUSES.INITIALIZED).length} /> + + plugin.status === PLUGIN_STATUSES.BLOCKED).length} /> +
+ + {error && ( +
+ {error} +
+ )} + {snapshot.plugins.length === 0 ? (
Plugin discovery is running. @@ -175,16 +445,143 @@ export default function PluginRegistryView({ placement = "settings" }: { placeme {plugin.error || plugin.id}
- +
+ {plugin.updateAvailable ? ( + Update available + ) : null} + +
))} )} - {widgets.map((widget: PluginWidget) => ( - - ))} +
+
+
+ Marketplace +
+ {snapshot.marketplace.map((plugin) => ( + + ))} + {snapshot.marketplace.length === 0 && ( +
+ No marketplace extensions available. +
+ )} +
+ +
+
+
+ Permission Review +
+ {selectedPlugin ? ( + <> +
+ {selectedPlugin.name} +
+
+ {selectedPlugin.description} +
+
+
+ Requested permissions +
+
+ {(selectedPlugin.permissionsRequested || []).map((permission) => ( + + ))} + {(!selectedPlugin.permissionsRequested || selectedPlugin.permissionsRequested.length === 0) && ( + + This extension requests no permissions. + + )} +
+
+
+
Version: {selectedPlugin.version}
+
Runtime: {selectedPlugin.runtime?.mode || "iframe"}
+
+ Widgets: {Array.isArray(selectedPlugin.widgets) ? selectedPlugin.widgets.length : 0} +
+
+ Dependencies: {(selectedPlugin.dependencies || []).length > 0 + ? selectedPlugin.dependencies.join(", ") + : "none"} +
+
+
+ + +
+ + ) : ( +
+ Select a marketplace extension to review its manifest, permissions, and sandbox mode before installation. +
+ )} +
+ + {widgets.map((widget: PluginWidget) => ( + + ))} +
+
); } diff --git a/src/components/dashboard/Settings.tsx b/src/components/dashboard/Settings.tsx index 16191b24..776b14c9 100644 --- a/src/components/dashboard/Settings.tsx +++ b/src/components/dashboard/Settings.tsx @@ -398,6 +398,14 @@ export default function Settings() { +
+

Extensions

+
+ Review installed plugins, manage permissions, and install sandboxed marketplace extensions. +
+ +
+
Export & Import
diff --git a/src/plugins/PluginManager.js b/src/plugins/PluginManager.js index 7fe229b7..cb43c0bb 100644 --- a/src/plugins/PluginManager.js +++ b/src/plugins/PluginManager.js @@ -1,13 +1,37 @@ import React from "react"; import { getEnvironmentConfig, loadConfigProfiles, getActiveProfileName } from "../lib/config"; -import { useStore } from "../lib/store"; // Assuming useStore can return the store instance +import { useStore } from "../lib/store"; +import { + loadInstalledPlugins, + upsertInstalledPlugin, + removeInstalledPlugin, + loadPermissionGrants, + savePermissionGrants, +} from "./pluginStorage"; +import { fetchMarketplacePlugins, fetchMarketplacePluginById } from "./pluginCatalog"; +import SandboxedPluginFrame from "./pluginSandbox"; const PLUGIN_STATUSES = Object.freeze({ REGISTERED: "registered", INITIALIZED: "initialized", FAILED: "failed", + BLOCKED: "blocked", + PENDING_REVIEW: "pending-review", + DISABLED: "disabled", }); +const ALLOWED_PERMISSION_SCOPES = Object.freeze([ + "dashboard:read", + "dashboard:write", + "data:read", + "data:write", + "notifications:write", + "network:request", + "storage:read", + "storage:write", + "window:open", +]); + const SAFE_STATE_KEYS = Object.freeze([ "network", "theme", @@ -23,6 +47,9 @@ const SAFE_STATE_KEYS = Object.freeze([ "walletPublicKey", "streamStatus", "streamLedgers", + "searchFilters", + "notificationHistory", + "unreadNotificationCount", ]); const SAFE_ACTION_KEYS = Object.freeze([ @@ -34,6 +61,13 @@ const SAFE_ACTION_KEYS = Object.freeze([ "removeNotification", ]); +const pluginModules = import.meta.glob("./**/*Plugin.{js,jsx,ts,tsx}", { + eager: false, +}); + +let registrationPromise = null; +let registrationComplete = false; + function freezePlainObject(value) { if (!value || typeof value !== "object") return value; if (Array.isArray(value)) return Object.freeze(value.map(freezePlainObject)); @@ -55,99 +89,202 @@ function pickSafeState(state) { } function normalizePlugin(rawPlugin) { - const plugin = rawPlugin?.default || rawPlugin; + const plugin = rawPlugin?.default || rawPlugin?.plugin || rawPlugin?.createPlugin || rawPlugin; if (typeof plugin === "function") return plugin(); return plugin; } -function normalizeWidget(widget, pluginRecord, index) { - if (!widget || typeof widget !== "object") return null; +function normalizeManifest(plugin) { + if (!plugin || typeof plugin !== "object") return null; + const manifest = plugin.manifest || plugin; + + if (!manifest.id || typeof manifest.id !== "string") return null; + if (!manifest.name || typeof manifest.name !== "string") return null; + + const permissions = Array.isArray(manifest.permissions) + ? manifest.permissions.filter( + (permission) => + typeof permission === "string" && ALLOWED_PERMISSION_SCOPES.includes(permission) + ) + : []; + + const dependencyPlugins = Array.isArray(manifest.dependencies?.plugins) + ? manifest.dependencies.plugins.filter((pluginId) => typeof pluginId === "string") + : []; + + return freezePlainObject({ + id: manifest.id, + name: manifest.name, + version: String(manifest.version || "1.0.0"), + description: String(manifest.description || ""), + author: manifest.author || null, + homepageUrl: manifest.homepageUrl || null, + permissions, + dependencies: { plugins: dependencyPlugins }, + runtime: { + mode: manifest.runtime?.mode || "module", + entry: manifest.runtime?.entry || null, + source: manifest.runtime?.source || null, + srcDoc: manifest.runtime?.srcDoc || null, + sandbox: Array.isArray(manifest.runtime?.sandbox) + ? manifest.runtime.sandbox.filter(Boolean) + : ["allow-scripts"], + }, + widgets: Array.isArray(manifest.widgets) ? manifest.widgets : [], + dataSources: Array.isArray(manifest.dataSources) ? manifest.dataSources : [], + }); +} + +function compareVersions(a, b) { + const parse = (value) => + String(value || "0.0.0") + .split(".") + .map((segment) => Number.parseInt(segment, 10) || 0); + + const left = parse(a); + const right = parse(b); + const length = Math.max(left.length, right.length); + + for (let index = 0; index < length; index += 1) { + const diff = (left[index] || 0) - (right[index] || 0); + if (diff !== 0) return diff; + } - const Component = widget.component || widget.Component || widget.render; - if (!Component) return null; + return 0; +} +function sanitizePermissions(permissions = []) { + return Array.from( + new Set( + (Array.isArray(permissions) ? permissions : []).filter((permission) => + ALLOWED_PERMISSION_SCOPES.includes(permission) + ) + ) + ); +} + +function hasPermission(record, permission) { + return Array.isArray(record.permissionsGranted) && record.permissionsGranted.includes(permission); +} + +function createFailureWidget(pluginId, name, message) { return { - id: String(widget.id || `${pluginRecord.id}:widget:${index}`), - pluginId: pluginRecord.id, - pluginName: pluginRecord.name, - title: widget.title || widget.name || pluginRecord.name, - placement: widget.placement || "settings", - order: Number.isFinite(widget.order) ? widget.order : 100, - props: widget.props || {}, - component: Component, + id: `${pluginId}:failure`, + title: `${name} unavailable`, + placement: "settings", + order: 999, + component: function PluginFailureWidget() { + return React.createElement( + "div", + { + style: { + color: "var(--red)", + fontSize: "12px", + lineHeight: 1.5, + }, + }, + message + ); + }, }; } -function normalizeDataSource(dataSource, pluginRecord, index) { - if (!dataSource || typeof dataSource !== "object") return null; +function createIframeRuntime(manifest) { return { - id: String(dataSource.id || `${pluginRecord.id}:data-source:${index}`), - pluginId: pluginRecord.id, - pluginName: pluginRecord.name, - name: dataSource.name || dataSource.id || `Data source ${index + 1}`, - description: dataSource.description || "", - fetch: typeof dataSource.fetch === "function" ? dataSource.fetch : null, - subscribe: typeof dataSource.subscribe === "function" ? dataSource.subscribe : null, - metadata: dataSource.metadata || {}, + initialize: async () => undefined, + getWidgets: () => manifest.widgets || [], + getDataSources: () => manifest.dataSources || [], }; } -function createFallbackPlugin(plugin, reason) { - const id = String(plugin?.id || `invalid-plugin-${Date.now()}`); - const name = String(plugin?.name || id); +function createModuleRuntimeLoader(manifest) { + if (!manifest.runtime?.entry) return null; + return async () => { + const imported = await import(/* @vite-ignore */ manifest.runtime.entry); + const runtime = normalizePlugin(imported); + if (typeof runtime === "function") { + return runtime(); + } + return runtime || {}; + }; +} + +function createRecord({ + manifest, + runtime, + runtimeLoader, + sourceType, + permissionsGranted, + enabled = true, + installedAt = null, + initializedAt = null, + status = PLUGIN_STATUSES.REGISTERED, + error = null, + runtimeLoaded = true, +}) { return { - id, - name, - initialize: () => undefined, - getWidgets: () => [ - { - id: `${id}:fallback`, - title: `${name} unavailable`, - placement: "settings", - order: 1000, - component: function PluginFallbackWidget() { - return React.createElement( - "div", - { - style: { - color: "var(--red)", - fontSize: "12px", - lineHeight: 1.5, - }, - }, - reason - ); - }, - }, - ], - getDataSources: () => [], + id: manifest.id, + name: manifest.name, + version: manifest.version, + manifest, + runtime, + runtimeLoader, + runtimeLoaded, + sourceType, + status, + error, + enabled, + installedAt, + initializedAt, + permissionsGranted: sanitizePermissions(permissionsGranted || manifest.permissions || []), + updateAvailable: false, + latestVersion: null, + dependencyStatus: null, }; } export class PluginManager { - constructor({ store = useStore() } = {}) { // useStore() to get the actual store instance - this.store = store; + constructor({ store = useStore } = {}) { + this.store = store && typeof store.getState === "function" ? store : useStore; this.plugins = new Map(); this.listeners = new Set(); this.initializing = null; + this.marketplace = new Map(); + this.hydrated = false; } - createDashboardApi(pluginId) { - const getCurrentState = () => pickSafeState(this.store.getState()); + createDashboardApi(pluginId, manifest) { + const currentState = this.store.getState(); + const permissions = Array.isArray(manifest?.permissions) ? manifest.permissions : []; - const actions = SAFE_ACTION_KEYS.reduce((safeActions, key) => { - const action = this.store.getState()[key]; - if (typeof action === "function") { - safeActions[key] = (...args) => action(...args); + const actions = {}; + if (permissions.includes("dashboard:write")) { + SAFE_ACTION_KEYS.forEach((key) => { + const action = currentState[key]; + if (typeof action === "function") { + actions[key] = (...args) => action(...args); + } + }); + } + + if (permissions.includes("notifications:write")) { + const addNotification = currentState.addNotification; + const removeNotification = currentState.removeNotification; + if (typeof addNotification === "function") { + actions.addNotification = (...args) => addNotification(...args); + } + if (typeof removeNotification === "function") { + actions.removeNotification = (...args) => removeNotification(...args); } - return safeActions; - }, {}); + } return Object.freeze({ pluginId, + manifest, + permissions: Object.freeze([...permissions]), version: "1.0.0", - getState: getCurrentState, + getState: () => pickSafeState(this.store.getState()), getConfig: () => freezePlainObject({ environment: getEnvironmentConfig(), @@ -156,7 +293,9 @@ export class PluginManager { }), actions: Object.freeze(actions), subscribe: (listener) => { - if (typeof listener !== "function") return () => {}; + if (typeof listener !== "function" || !permissions.includes("dashboard:read")) { + return () => {}; + } return this.store.subscribe((state) => listener(pickSafeState(state))); }, logger: Object.freeze({ @@ -167,38 +306,214 @@ export class PluginManager { }); } - register(rawPlugin) { + emitChange() { + this.listeners.forEach((listener) => listener(this)); + } + + subscribe(listener) { + if (typeof listener !== "function") return () => {}; + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + validate(plugin) { + if (!plugin || typeof plugin !== "object") { + return "Plugin export must be an object or factory."; + } + + const manifest = normalizeManifest(plugin); + if (!manifest) return "Plugin manifest is invalid or incomplete."; + if (plugin.initialize && typeof plugin.initialize !== "function") { + return "Plugin initialize hook must be a function."; + } + if (plugin.getWidgets && typeof plugin.getWidgets !== "function") { + return "Plugin getWidgets hook must be a function."; + } + if (plugin.getDataSources && typeof plugin.getDataSources !== "function") { + return "Plugin getDataSources hook must be a function."; + } + + return null; + } + + getRecord(pluginId) { + return this.plugins.get(pluginId) || null; + } + + getMarketplaceCache() { + return Array.from(this.marketplace.values()); + } + + async hydrateInstalledPlugins() { + if (this.hydrated) return; + this.hydrated = true; + + const installed = loadInstalledPlugins(); + for (const installedRecord of installed) { + if (!installedRecord || typeof installedRecord !== "object") continue; + const manifest = normalizeManifest(installedRecord.manifest); + if (!manifest) continue; + + if (this.plugins.has(manifest.id)) { + continue; + } + + const runtime = + manifest.runtime.mode === "iframe" + ? createIframeRuntime(manifest) + : { + initialize: async () => undefined, + getWidgets: () => manifest.widgets || [], + getDataSources: () => manifest.dataSources || [], + }; + + const runtimeLoader = + manifest.runtime.mode === "module" ? createModuleRuntimeLoader(manifest) : null; + + const record = createRecord({ + manifest, + runtime, + runtimeLoader, + sourceType: installedRecord.sourceType || "installed", + permissionsGranted: installedRecord.permissionsGranted || manifest.permissions || [], + enabled: installedRecord.enabled !== false, + installedAt: installedRecord.installedAt || new Date().toISOString(), + initializedAt: installedRecord.initializedAt || null, + status: installedRecord.enabled === false ? PLUGIN_STATUSES.DISABLED : PLUGIN_STATUSES.REGISTERED, + error: installedRecord.error || null, + runtimeLoaded: manifest.runtime.mode !== "module" || !runtimeLoader, + }); + + this.plugins.set(record.id, record); + } + } + + persistRecord(record) { + if (record.sourceType === "builtin") return; + + upsertInstalledPlugin({ + id: record.id, + name: record.name, + version: record.version, + manifest: record.manifest, + permissionsGranted: record.permissionsGranted, + enabled: record.enabled, + installedAt: record.installedAt, + initializedAt: record.initializedAt, + error: record.error, + sourceType: record.sourceType, + }); + + const grants = loadPermissionGrants(); + savePermissionGrants({ + ...grants, + [record.id]: record.permissionsGranted, + }); + } + + removePersistedRecord(pluginId) { + removeInstalledPlugin(pluginId); + const grants = loadPermissionGrants(); + if (grants && Object.prototype.hasOwnProperty.call(grants, pluginId)) { + const next = { ...grants }; + delete next[pluginId]; + savePermissionGrants(next); + } + } + + register(rawPlugin, options = {}) { const plugin = normalizePlugin(rawPlugin); const validationError = this.validate(plugin); - const safePlugin = validationError ? createFallbackPlugin(plugin, validationError) : plugin; - const id = String(safePlugin.id); + const safePlugin = validationError + ? { + id: String(plugin?.id || `invalid-plugin-${Date.now()}`), + name: String(plugin?.name || plugin?.id || "Invalid plugin"), + version: "0.0.0", + permissions: [], + runtime: { mode: "iframe" }, + widgets: [ + createFailureWidget( + String(plugin?.id || "invalid"), + String(plugin?.name || "Invalid"), + validationError + ), + ], + dataSources: [], + initialize: async () => undefined, + getWidgets: () => [ + createFailureWidget( + String(plugin?.id || "invalid"), + String(plugin?.name || "Invalid"), + validationError + ), + ], + getDataSources: () => [], + } + : plugin; - if (this.plugins.has(id)) { - throw new Error(`Plugin ID conflict: "${id}" is already registered.`); + const manifest = normalizeManifest(safePlugin); + if (!manifest) { + throw new Error("Plugin manifest is invalid."); } - const record = { - id, - name: String(safePlugin.name || id), - plugin: safePlugin, - status: PLUGIN_STATUSES.REGISTERED, - error: validationError || null, - initializedAt: null, - }; + if (this.plugins.has(manifest.id)) { + throw new Error(`Plugin ID conflict: "${manifest.id}" is already registered.`); + } + + const runtime = + options.runtime || + safePlugin || + (manifest.runtime.mode === "iframe" ? createIframeRuntime(manifest) : null); + + const runtimeLoader = + options.runtimeLoader || + (manifest.runtime.mode === "module" ? createModuleRuntimeLoader(manifest) : null); + + const record = createRecord({ + manifest, + runtime, + runtimeLoader, + sourceType: options.sourceType || "builtin", + permissionsGranted: options.permissionsGranted || manifest.permissions || [], + enabled: options.enabled !== false, + installedAt: options.installedAt || null, + initializedAt: options.initializedAt || null, + status: options.enabled === false ? PLUGIN_STATUSES.DISABLED : PLUGIN_STATUSES.REGISTERED, + error: validationError || options.error || null, + runtimeLoaded: options.runtimeLoaded !== undefined ? options.runtimeLoaded : manifest.runtime.mode !== "module" || !runtimeLoader, + }); - this.plugins.set(id, record); + this.plugins.set(record.id, record); this.emitChange(); return record; } - validate(plugin) { - if (!plugin || typeof plugin !== "object") return "Plugin export must be an object or factory."; - if (!plugin.id || typeof plugin.id !== "string") return "Plugin is missing a string id."; - if (!plugin.name || typeof plugin.name !== "string") return "Plugin is missing a string name."; - if (plugin.initialize && typeof plugin.initialize !== "function") return "Plugin initialize hook must be a function."; - if (plugin.getWidgets && typeof plugin.getWidgets !== "function") return "Plugin getWidgets hook must be a function."; - if (plugin.getDataSources && typeof plugin.getDataSources !== "function") return "Plugin getDataSources hook must be a function."; - return null; + canActivate(record) { + if (!record.enabled) { + return { ok: false, reason: "Plugin is disabled." }; + } + + const missingDependencies = (record.manifest.dependencies?.plugins || []).filter( + (dependencyId) => !this.plugins.has(dependencyId) + ); + if (missingDependencies.length > 0) { + return { + ok: false, + reason: `Missing plugin dependencies: ${missingDependencies.join(", ")}`, + }; + } + + const missingPermissions = (record.manifest.permissions || []).filter( + (permission) => !hasPermission(record, permission) + ); + if (missingPermissions.length > 0) { + return { + ok: false, + reason: `Missing permissions: ${missingPermissions.join(", ")}`, + }; + } + + return { ok: true }; } async initializeAll() { @@ -206,29 +521,84 @@ export class PluginManager { this.initializing = Promise.all( Array.from(this.plugins.values()).map(async (record) => { - if (record.status === PLUGIN_STATUSES.INITIALIZED) return record; + const activation = this.canActivate(record); + if (!activation.ok) { + record.status = record.enabled ? PLUGIN_STATUSES.BLOCKED : PLUGIN_STATUSES.DISABLED; + record.error = activation.reason; + return record; + } + + if (record.status === PLUGIN_STATUSES.INITIALIZED && record.runtimeLoaded) { + return record; + } try { - const api = this.createDashboardApi(record.id); - if (typeof record.plugin.initialize === "function") { - await record.plugin.initialize(api); + if (record.runtimeLoader && !record.runtimeLoaded) { + record.status = PLUGIN_STATUSES.REGISTERED; + record.runtime = await record.runtimeLoader(); + record.runtimeLoaded = true; } + + const api = this.createDashboardApi(record.id, record.manifest); + if (typeof record.runtime?.initialize === "function") { + await record.runtime.initialize(api); + } + record.status = PLUGIN_STATUSES.INITIALIZED; record.initializedAt = new Date().toISOString(); + record.error = null; + this.persistRecord(record); } catch (error) { record.status = PLUGIN_STATUSES.FAILED; record.error = error?.message || String(error); } - this.emitChange(); + return record; }) ).finally(() => { this.initializing = null; + this.refreshMarketplaceSnapshot().catch(() => {}); + this.emitChange(); }); return this.initializing; } + async refreshMarketplaceSnapshot() { + const catalog = await fetchMarketplacePlugins(); + this.marketplace = new Map(catalog.map((plugin) => [plugin.id, plugin])); + + for (const record of this.plugins.values()) { + const marketplacePlugin = this.marketplace.get(record.id); + record.latestVersion = marketplacePlugin?.version || record.version; + record.updateAvailable = + !!marketplacePlugin && compareVersions(marketplacePlugin.version, record.version) > 0; + } + + this.emitChange(); + return this.getMarketplacePlugins(); + } + + async getMarketplacePlugins() { + if (this.marketplace.size === 0) { + await this.refreshMarketplaceSnapshot(); + } + + return Array.from(this.marketplace.values()).map((plugin) => { + const installed = this.plugins.get(plugin.id); + const updateAvailable = installed + ? compareVersions(plugin.version, installed.version) > 0 + : false; + + return { + ...plugin, + installed: Boolean(installed), + installedVersion: installed?.version || null, + updateAvailable, + }; + }); + } + getPluginRecords() { return Array.from(this.plugins.values()).map((record) => ({ id: record.id, @@ -236,17 +606,32 @@ export class PluginManager { status: record.status, error: record.error, initializedAt: record.initializedAt, + version: record.version, + latestVersion: record.latestVersion || record.version, + updateAvailable: Boolean(record.updateAvailable), + sourceType: record.sourceType, + enabled: record.enabled, + permissionsGranted: record.permissionsGranted, + permissionsRequested: record.manifest.permissions || [], + dependencies: record.manifest.dependencies?.plugins || [], + installedAt: record.installedAt, })); } getWidgets({ placement } = {}) { return Array.from(this.plugins.values()) .flatMap((record) => { + if (record.status === PLUGIN_STATUSES.FAILED || record.status === PLUGIN_STATUSES.DISABLED) { + return []; + } + try { const widgets = - typeof record.plugin.getWidgets === "function" ? record.plugin.getWidgets() : []; + typeof record.runtime?.getWidgets === "function" + ? record.runtime.getWidgets() + : record.manifest.widgets || []; return widgets - .map((widget, index) => normalizeWidget(widget, record, index)) + .map((widget, index) => this.normalizeWidget(widget, record, index)) .filter(Boolean); } catch (error) { record.error = error?.message || String(error); @@ -258,13 +643,52 @@ export class PluginManager { .sort((a, b) => a.order - b.order || a.title.localeCompare(b.title)); } + normalizeWidget(widget, record, index) { + if (!widget || typeof widget !== "object") return null; + + const Component = widget.component || widget.Component || widget.render; + const isIframeWidget = widget.kind === "iframe" || (!Component && record.manifest.runtime.mode === "iframe"); + + if (!Component && !isIframeWidget) { + return null; + } + + return { + id: String(widget.id || `${record.id}:widget:${index}`), + pluginId: record.id, + pluginName: record.name, + title: widget.title || widget.name || record.name, + placement: widget.placement || "settings", + order: Number.isFinite(widget.order) ? widget.order : 100, + props: widget.props || {}, + component: isIframeWidget + ? (iframeProps) => + React.createElement(SandboxedPluginFrame, { + title: widget.title || widget.name || record.name, + description: widget.description || record.manifest.description, + src: widget.src || record.manifest.runtime.entry || undefined, + srcDoc: widget.srcDoc || record.manifest.runtime.srcDoc || undefined, + sandbox: widget.sandbox || record.manifest.runtime.sandbox, + height: widget.height || 220, + ...iframeProps, + }) + : Component, + }; + } + getDataSources() { return Array.from(this.plugins.values()).flatMap((record) => { + if (record.status === PLUGIN_STATUSES.FAILED || record.status === PLUGIN_STATUSES.DISABLED) { + return []; + } + try { const dataSources = - typeof record.plugin.getDataSources === "function" ? record.plugin.getDataSources() : []; + typeof record.runtime?.getDataSources === "function" + ? record.runtime.getDataSources() + : record.manifest.dataSources || []; return dataSources - .map((dataSource, index) => normalizeDataSource(dataSource, record, index)) + .map((dataSource, index) => this.normalizeDataSource(dataSource, record, index)) .filter(Boolean); } catch (error) { record.error = error?.message || String(error); @@ -274,16 +698,179 @@ export class PluginManager { }); } - subscribe(listener) { - if (typeof listener !== "function") return () => {}; - this.listeners.add(listener); - return () => this.listeners.delete(listener); + normalizeDataSource(dataSource, record, index) { + if (!dataSource || typeof dataSource !== "object") return null; + return { + id: String(dataSource.id || `${record.id}:data-source:${index}`), + pluginId: record.id, + pluginName: record.name, + name: dataSource.name || dataSource.id || `Data source ${index + 1}`, + description: dataSource.description || "", + fetch: typeof dataSource.fetch === "function" ? dataSource.fetch : null, + subscribe: typeof dataSource.subscribe === "function" ? dataSource.subscribe : null, + metadata: dataSource.metadata || {}, + }; } - emitChange() { - this.listeners.forEach((listener) => listener(this)); + async installPlugin(manifest, { approvedPermissions } = {}) { + const normalizedManifest = normalizeManifest(manifest); + if (!normalizedManifest) { + throw new Error("Cannot install an invalid plugin manifest."); + } + + const existing = this.plugins.get(normalizedManifest.id); + if (existing?.sourceType === "builtin") { + throw new Error(`"${normalizedManifest.id}" is reserved by a built-in plugin.`); + } + if (existing?.sourceType === "installed") { + await this.uninstallPlugin(normalizedManifest.id); + } + + const requested = sanitizePermissions(normalizedManifest.permissions || []); + const granted = sanitizePermissions(approvedPermissions || requested); + + if (requested.some((permission) => !granted.includes(permission))) { + throw new Error("Permission review required before installing this plugin."); + } + + const runtime = + normalizedManifest.runtime.mode === "iframe" + ? createIframeRuntime(normalizedManifest) + : null; + const runtimeLoader = + normalizedManifest.runtime.mode === "module" + ? createModuleRuntimeLoader(normalizedManifest) + : null; + + const record = createRecord({ + manifest: normalizedManifest, + runtime, + runtimeLoader, + sourceType: "installed", + permissionsGranted: granted, + enabled: true, + installedAt: new Date().toISOString(), + status: PLUGIN_STATUSES.REGISTERED, + runtimeLoaded: normalizedManifest.runtime.mode !== "module" || !runtimeLoader, + }); + + this.plugins.set(record.id, record); + this.persistRecord(record); + await this.initializeAll(); + return record; + } + + async updatePlugin(pluginId) { + const current = this.plugins.get(pluginId); + if (!current) { + throw new Error(`Plugin "${pluginId}" is not installed.`); + } + + const marketplacePlugin = await fetchMarketplacePluginById(pluginId); + if (!marketplacePlugin) { + throw new Error(`No marketplace update was found for "${pluginId}".`); + } + + const currentPermissions = current.permissionsGranted || []; + const requested = sanitizePermissions(marketplacePlugin.permissions || []); + const missingPermissions = requested.filter((permission) => !currentPermissions.includes(permission)); + if (missingPermissions.length > 0) { + throw new Error( + `Plugin update requires additional permissions: ${missingPermissions.join(", ")}` + ); + } + + return this.installPlugin(marketplacePlugin, { approvedPermissions: currentPermissions }); + } + + async uninstallPlugin(pluginId) { + this.plugins.delete(pluginId); + this.removePersistedRecord(pluginId); + this.emitChange(); + } + + async setPluginEnabled(pluginId, enabled) { + const record = this.plugins.get(pluginId); + if (!record) { + throw new Error(`Plugin "${pluginId}" is not installed.`); + } + + record.enabled = Boolean(enabled); + record.status = record.enabled ? PLUGIN_STATUSES.REGISTERED : PLUGIN_STATUSES.DISABLED; + record.error = null; + this.persistRecord(record); + await this.initializeAll(); + return record; + } + + async bootstrap() { + await this.hydrateInstalledPlugins(); + await this.refreshMarketplaceSnapshot(); + return this; } } export const pluginManager = new PluginManager(); + +export async function registerActivePlugins(manager = pluginManager) { + if (registrationComplete) return manager; + if (registrationPromise) return registrationPromise; + + registrationPromise = (async () => { + await manager.hydrateInstalledPlugins(); + + await Promise.all( + Object.entries(pluginModules).map(async ([path, loadModule]) => { + try { + const module = await loadModule(); + const pluginFactory = normalizePlugin(module); + if (!pluginFactory) { + manager.register( + { + id: path.replace(/[^a-z0-9]+/gi, "-").toLowerCase(), + name: path, + version: "0.0.0", + runtime: { mode: "iframe" }, + widgets: [], + dataSources: [], + }, + { sourceType: "builtin" } + ); + return; + } + + manager.register(pluginFactory, { sourceType: "builtin" }); + } catch (error) { + const id = path.replace(/[^a-z0-9]+/gi, "-").toLowerCase(); + manager.register( + { + id, + name: path, + version: "0.0.0", + runtime: { mode: "iframe" }, + widgets: [ + createFailureWidget(id, path, error?.message || String(error)), + ], + dataSources: [], + initialize: async () => { + throw error; + }, + }, + { sourceType: "builtin", error: error?.message || String(error) } + ); + } + }) + ); + + await manager.bootstrap(); + await manager.initializeAll(); + registrationComplete = true; + return manager; + })().finally(() => { + registrationPromise = null; + }); + + return registrationPromise; +} + export { PLUGIN_STATUSES }; diff --git a/src/plugins/__tests__/PluginManager.test.js b/src/plugins/__tests__/PluginManager.test.js new file mode 100644 index 00000000..2f52bb0c --- /dev/null +++ b/src/plugins/__tests__/PluginManager.test.js @@ -0,0 +1,85 @@ +import { describe, expect, it, beforeEach, vi } from "vitest"; +import { PluginManager, PLUGIN_STATUSES } from "../PluginManager"; + +function createMockStore() { + const listeners = new Set(); + let state = { + network: "testnet", + theme: "dark", + activeTab: "overview", + connectedAddress: "GTEST", + }; + + return { + getState: () => state, + setState: (nextState) => { + state = { ...state, ...nextState }; + listeners.forEach((listener) => listener(state)); + }, + subscribe: (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + }; +} + +const iframeManifest = { + id: "community.test-extension", + name: "Test Extension", + version: "1.0.0", + description: "A sandboxed extension used in unit tests.", + permissions: ["dashboard:read"], + runtime: { + mode: "iframe", + sandbox: ["allow-scripts"], + srcDoc: "

Test extension

", + }, + widgets: [ + { + id: "community.test-extension.settings", + title: "Test Extension", + placement: "settings", + kind: "iframe", + }, + ], + dataSources: [], +}; + +describe("PluginManager", () => { + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it("installs sandboxed manifests, persists them, and exposes widgets", async () => { + const manager = new PluginManager({ store: createMockStore() }); + + await manager.installPlugin(iframeManifest); + + const records = manager.getPluginRecords(); + expect(records).toHaveLength(1); + expect(records[0].id).toBe(iframeManifest.id); + expect(records[0].status).toBe(PLUGIN_STATUSES.INITIALIZED); + + const widgets = manager.getWidgets({ placement: "settings" }); + expect(widgets).toHaveLength(1); + expect(widgets[0].pluginId).toBe(iframeManifest.id); + + const installedSnapshot = JSON.parse(localStorage.getItem("stellar-dashboard:plugins:v1")); + expect(installedSnapshot.installed).toHaveLength(1); + expect(installedSnapshot.installed[0].manifest.id).toBe(iframeManifest.id); + }); + + it("hydrates installed plugins from storage and removes them cleanly", async () => { + const manager = new PluginManager({ store: createMockStore() }); + + await manager.installPlugin(iframeManifest); + await manager.uninstallPlugin(iframeManifest.id); + + expect(manager.getPluginRecords()).toHaveLength(0); + + const secondManager = new PluginManager({ store: createMockStore() }); + await secondManager.hydrateInstalledPlugins(); + expect(secondManager.getPluginRecords()).toHaveLength(0); + }); +}); diff --git a/src/plugins/index.js b/src/plugins/index.js index 1118f3ee..0609b644 100644 --- a/src/plugins/index.js +++ b/src/plugins/index.js @@ -1,81 +1,16 @@ -import { PluginManager } from "./PluginManager"; -import React from "react"; +export { + PluginManager, + pluginManager, + registerActivePlugins, + PLUGIN_STATUSES, +} from "./PluginManager"; -const pluginModules = import.meta.glob("./**/*Plugin.{js,jsx,ts,tsx}", { - eager: false, -}); +export { + fetchMarketplacePlugins, + fetchMarketplacePluginById, + getMarketplacePluginIndex, + listMarketplacePluginSummaries, + MARKETPLACE_PLUGINS, +} from "./pluginCatalog"; -let registrationPromise = null; - -function getPluginFactory(module) { - return module?.default || module?.plugin || module?.createPlugin || null; -} - -function pathToPluginId(prefix, path) { - return `${prefix}.${path.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase()}`; -} - -function registerWithFallback(manager, plugin, path) { - try { - manager.register(plugin); - } catch (error) { - manager.register({ - id: pathToPluginId("conflict", path), - name: `${path} registration conflict`, - initialize: () => undefined, - getWidgets: () => [ - { - id: `${pathToPluginId("conflict", path)}.widget`, - title: "Plugin registration conflict", - placement: "settings", - order: 1000, - component: function PluginConflictWidget() { - return React.createElement( - "div", - { style: { color: "var(--red)", fontSize: "12px" } }, - error?.message || String(error) - ); - }, - }, - ], - getDataSources: () => [], - }); - } -} - -export async function registerActivePlugins(manager = pluginManager) { - if (registrationPromise) return registrationPromise; - registrationPromise = Promise.all( - Object.entries(pluginModules).map(async ([path, loadModule]) => { - try { - const module = await loadModule(); - const pluginFactory = getPluginFactory(module); - if (!pluginFactory) { - registerWithFallback(manager, { - id: pathToPluginId("invalid", path), - name: path, - getWidgets: () => [], - getDataSources: () => [], - }, path); - return; - } - registerWithFallback(manager, pluginFactory, path); - } catch (error) { - const id = pathToPluginId("failed", path); - registerWithFallback(manager, { - id, - name: path, - initialize: () => { - throw error; - }, - getWidgets: () => [], - getDataSources: () => [], - }, path); - } - }) - ) - .then(() => manager.initializeAll()) - .then(() => manager); - return registrationPromise; -} -export const pluginManager = new PluginManager(); +export { loadInstalledPlugins, loadPermissionGrants } from "./pluginStorage"; diff --git a/src/plugins/pluginCatalog.js b/src/plugins/pluginCatalog.js new file mode 100644 index 00000000..9dfd0ea8 --- /dev/null +++ b/src/plugins/pluginCatalog.js @@ -0,0 +1,220 @@ +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +function makeIframeSrcDoc({ title, accent, body, footer }) { + return ` + + + + + + + +
+
+
${title}
+

${title}

+

${body}

+
Sandboxed extension preview
+
+ +
+ +`; +} + +const MARKETPLACE_PLUGINS = [ + { + id: "community.activity-radar", + name: "Activity Radar", + version: "1.2.0", + description: + "Surfaces recent dashboard activity in a compact, permission-scoped overview card.", + author: "Community Labs", + homepageUrl: "https://example.com/activity-radar", + permissions: ["dashboard:read", "data:read"], + runtime: { + mode: "iframe", + sandbox: ["allow-scripts"], + srcDoc: makeIframeSrcDoc({ + title: "Activity Radar", + accent: "#66d9ef", + body: + "Tracks the current network, active tab, and high-signal events without exposing write access to the host app.", + footer: "Permissions: dashboard:read, data:read", + }), + }, + widgets: [ + { + id: "community.activity-radar.settings", + title: "Activity Radar", + placement: "settings", + order: 5, + kind: "iframe", + height: 220, + }, + ], + dataSources: [], + }, + { + id: "community.security-beacon", + name: "Security Beacon", + version: "1.0.3", + description: + "Highlights permission grants, plugin updates, and sensitive dashboard actions.", + author: "Shield Collective", + homepageUrl: "https://example.com/security-beacon", + permissions: ["dashboard:read", "notifications:write"], + runtime: { + mode: "iframe", + sandbox: ["allow-scripts"], + srcDoc: makeIframeSrcDoc({ + title: "Security Beacon", + accent: "#f97316", + body: + "Uses the plugin permission model to show how third-party extensions can request only the access they need.", + footer: "Permissions: dashboard:read, notifications:write", + }), + }, + widgets: [ + { + id: "community.security-beacon.settings", + title: "Security Beacon", + placement: "settings", + order: 12, + kind: "iframe", + height: 220, + }, + ], + dataSources: [], + }, + { + id: "community.network-snapshot", + name: "Network Snapshot", + version: "0.9.8", + description: + "A sandboxed summary card that demonstrates dependency metadata and version management.", + author: "Open Extensions", + homepageUrl: "https://example.com/network-snapshot", + permissions: ["dashboard:read"], + dependencies: { plugins: ["core.runtime-status"] }, + runtime: { + mode: "iframe", + sandbox: ["allow-scripts"], + srcDoc: makeIframeSrcDoc({ + title: "Network Snapshot", + accent: "#34d399", + body: + "Uses the built-in runtime-status plugin as a dependency and shows how plugin manifests can declare load-order constraints.", + footer: "Dependency: core.runtime-status", + }), + }, + widgets: [ + { + id: "community.network-snapshot.settings", + title: "Network Snapshot", + placement: "settings", + order: 20, + kind: "iframe", + height: 220, + }, + ], + dataSources: [], + }, +]; + +function clonePluginManifest(plugin) { + return JSON.parse(JSON.stringify(plugin)); +} + +export async function fetchMarketplacePlugins() { + await delay(200); + return MARKETPLACE_PLUGINS.map(clonePluginManifest); +} + +export async function fetchMarketplacePluginById(pluginId) { + const catalog = await fetchMarketplacePlugins(); + return catalog.find((plugin) => plugin.id === pluginId) || null; +} + +export function getMarketplacePluginIndex(pluginId) { + return MARKETPLACE_PLUGINS.findIndex((plugin) => plugin.id === pluginId); +} + +export function listMarketplacePluginSummaries() { + return MARKETPLACE_PLUGINS.map(({ runtime, ...plugin }) => ({ + ...plugin, + permissionCount: plugin.permissions.length, + widgetCount: Array.isArray(plugin.widgets) ? plugin.widgets.length : 0, + executionMode: runtime?.mode || "iframe", + })); +} + +export { MARKETPLACE_PLUGINS }; diff --git a/src/plugins/pluginSandbox.jsx b/src/plugins/pluginSandbox.jsx new file mode 100644 index 00000000..45738dc4 --- /dev/null +++ b/src/plugins/pluginSandbox.jsx @@ -0,0 +1,84 @@ +import React from "react"; + +function buildSandboxAttribute(sandbox) { + const tokens = Array.isArray(sandbox) ? sandbox.filter(Boolean) : []; + return tokens.length ? tokens.join(" ") : "allow-scripts"; +} + +function buildFallbackSrcDoc(title, description) { + const safeTitle = String(title || "Plugin").replace(/ + + + + + + + +
+

${safeTitle}

+

${safeDescription}

+
+ +`; +} + +export default function SandboxedPluginFrame({ + title, + description, + src, + srcDoc, + sandbox, + height = 220, +}) { + const iframeSrcDoc = srcDoc || buildFallbackSrcDoc(title, description); + const iframeSandbox = buildSandboxAttribute(sandbox); + + return ( +