Submitting and reviewing plugins (open ecosystem)
Minibar plugins are npm packages: the host downloads them from npm or a nearby mirror and checks each one against the name + version + sha256 pinned in the signed index (10-backend.md §2b). The index (registry.json) is hosted on Vercel (https://minibar.084817.xyz; the server lives in server/). Bundled plugins come from Sources/BundledMiniApps/Catalog/ and are published by CI as @minibar-app/*; community plugins are published to npm by their authors, with a source copy under registry/plugins/<plugin id>/ in this repository for review and index builds. The app's Market shows a "Community" badge on the latter and npm download counts for every plugin.
1. How to submit
- Scaffold with
minibar-cli initor copy one ofdocs/examples/(including apackage.json:versionequal to the manifest's,keywordswithminibar-plugin, noscripts), then make it pass locally:node tools/jsharness/run.js <plugin dir> # render under the mock host node tools/jsharness/scan.js --strict <plugin dir> # static scan with the registry rules, package.json includedTry it on a Mac through Settings › Plugins › Install from Folder… or developer mode.
- Publish to npm:
npm publish --access public(any name works;minibar-plugin-<name>is the convention). The index pins the exact version and the whole tarball's hash, and npm never lets a published version change, so updates are new versions. - Put the plugin source under
registry/plugins/<plugin id>/(the directory name must equal the manifestid) and open a pull request (while the repository is private, send it through the channel named on the site's support page). The description must answer:- what the plugin does and where its data comes from;
- what each permission is for (especially
system,apps,files,clipboard); - what each entry in
allowedHostsis for;"*"needs its own justification; - whether third-party code (libraries, build output) is included, with source and license.
- CI (
.github/workflows/plugin-submission.yml) runs the static scan (--strict), a render under the mock host and a dry-run index build. Green is the precondition for review. - A maintainer reviews against the checklist in §3 and confirms that the tarball published on npm matches the source in the PR (
scripts/build_registry.py --resolve-from https://registry.npmjs.orgdownloads the published package, checks its manifest and computes the hash). Approval means merge; the Registry workflow then deploys the index, and apps see the plugin on their next refresh (cached 6 hours, immediately when the Market window opens). - Updates: publish the new version to npm, bump
version(semver) and open another PR; the app offers the update, and users can turn on automatic updates.
2. What the static scanner checks (tools/jsharness/scan.js)
| Area | Rule | Result |
|---|---|---|
| manifest | reverse-DNS id, directory = id, semver version, name ≤ 32, description ≤ 200, unknown permissions, network without allowedHosts, forbidden allowedSchemes | error |
| manifest | missing author / homepage (--strict), allowedHosts: ["*"], asking for system / apps / files / clipboard | error / warning (review focus) |
| files | extension allowlist (same as PathSafety.allowedExtensions), symlinks, > 8 directory levels, a file > 5 MB, > 20 MB in total, > 200 files | error |
| scripts | touching __host / __runtime / __meta, eval, Function constructor, require / import(), XMLHttpRequest / WebSocket, Node process, browser storage | error (no legitimate use) |
| scripts | entry without defineMiniApp, script > 1 MB | error |
| scripts | very long lines (minified / obfuscated), long String.fromCharCode chains, runs of \x escapes, fetch / atob / setTimeout (absent in the runtime), clipboard reads, http URLs or hosts outside allowedHosts in the source | warning |
| web cards | <script src="http…"> | error; other network references are warnings |
The scanner is the first gate: it keeps out what can never be legitimate and marks what a human must look at.
3. Reviewer checklist
Tick every item before merging:
- [ ] Does only what it says: read
main.js(usually < 300 lines); everyctx.httphost and its purpose match the PR; no user data (clipboard, calendar, file contents, location) goes to any host unless that is the plugin's stated core function. - [ ] Least privilege: every permission is actually used;
system/appscalls sit in actions behind a button the user understands;filesreads only what the user picked; a clipboard read has a clear reason. - [ ] Precise
allowedHosts: no"*"; no hosts unrelated to the feature; no URL shorteners or redirectors. - [ ] No obfuscation: readable code; third-party libraries (if any) are unmodified known versions with source and license; minified output comes with the unminified source or a reproducible build.
- [ ] Honest UI: headline and card do not imitate system alerts; button labels say what happens; no Apple or third-party trademarks as icons.
- [ ] Stable: renders offline under the mock host without errors; network failure degrades gracefully;
refreshInterval≥ 60 unless justified (5 s is the hard floor). - [ ] Description and localization: clear
name/description; translations inlocalizationsmatch the original. - [ ] Traceable:
homepageis a reachable repository or page and the author can be contacted.
Common reasons for rejection: allowedHosts: ["*"] without justification, personal data sent to third parties, obfuscated code, a duplicate of a bundled plugin without improvements.
4. Revoking a plugin, and the drill
registry/revoked.json:
{ "revoked": [ { "id": "com.example.bad", "reason": "Sends clipboard contents to a third party" } ] }
After the merge the Registry workflow republishes registry.json. Every Mac with the plugin installed, on its next registry refresh (≤ 24 h, immediately when the Market opens):
- marks it "Removed from the market: <reason>" and refuses installs;
- auto-disables the installed copy (the card shows the reason; the user can view the source, uninstall, or knowingly re-enable);
- writes a log line.
Once the entry is removed from revoked.json, the next registry refresh resumes the paused copy by itself (only pauses caused by the revocation; pauses for crashes or timeouts stay, and the user's own enabled/disabled choice is kept).
Drill (once before each release, to prove the chain works):
- On a branch, add
app.minibar.community.worldclocktorevoked.json; runnode server/dev.mjs --build(builds and serves the registry on this Mac;curl http://127.0.0.1:8000/registry.jsonshows it underrevoked; plain http is accepted only from loopback addresses, and a local registry is unsigned, so the Market says "Unverified source"); - Set Settings › General › registry URL to
http://127.0.0.1:8000/registry.jsonand open the Market: World Clock shows as removed and the installed copy is paused; - Point the URL back at the real registry and confirm it recovers;
- Do not merge the drill branch.
4a. Not aiming for the official source? Run an external one
The official source only lists reviewed plugins. Team, private or experimental plugins can live in a source of your own: build and host a registry.json as described in plugin-guide.en.md §11, and users add its address under Settings › Market › External sources. Plugins from external sources wear a source tag and show as unsigned, but installs are verified, sandboxed and permission-gated exactly like official ones.
5. Registry settings on the maintainer side
- Hosted on the Vercel project
minibar-registry;registry.ymldeploys with theVERCEL_TOKEN/VERCEL_ORG_ID/VERCEL_PROJECT_IDsecrets (default branch → production, other branches → preview). Details in10-backend.md§5. - The
MINIBAR_REGISTRY_KEYsecret holds the Ed25519 private key (scripts/sign-registry.sh keygen); the public key is embedded in the app'sRegistryTrust. The app refuses an unsigned registry from the official host; other sources may be unsigned and show "Unverified source". --featuredcontrols the Market's front page.