1. Context & Problem Statement
Currently, the Joplin plugin ecosystem relies on a scheduled cron job that automatically pulls new plugins from the NPM registry every 30 minutes. This "pull-based, trust-by-default" architecture lacks automated security scanning, human review verification, and traceability back to the developer's original source code. If a compromised or malicious package is published to NPM, it is blindly ingested into the Joplin ecosystem.
The objective of this project is to transition the ecosystem to an "event-driven, verify-by-default" model. This will secure the supply chain and introduce automated malware scanning without sacrificing the frictionless Developer Experience (DX) that Joplin plugin creators currently rely on.
2. Proposed Architecture & Developer Experience (DX)
To achieve a secure pipeline without forcing developers to learn new workflows or install global tools, this architecture integrates directly into the native NPM lifecycle.
By using the prepublishOnly hook within the plugin's package.json template, we can securely intercept the developer's standard npm publish command. This approach maintains 100% backward compatibility. When triggered, the hook runs a bundled @joplin/plugin-repo-cli dependency that performs local metadata validation, authenticates the user via a GitHub Device Flow, and securely submits the repository state to a structured GitHub Issue.
To create a strict security failsafe, the script intentionally exits with a non-zero code after a successful Joplin submission. This locally crashes the final step of the NPM PUBLISH process, effectively blocking the plugin code from ever leaking to the public NPM registry.
To ensure a smooth Developer Experience (DX), the @joplin/plugin-repo-cli will intercept the standard NPM error stream. Before triggering the intentional exit 1, the CLI will output a clear, formatted success block in the terminal containing the URL of the newly created GitHub Issue. This prevents developers from seeing a "raw" crash and clarifies that the "NPM ERR!" is a security feature, not a bug.
Opt-in Privacy Strategy The yo joplin generator will include a configuration prompt: "Do you want to mirror this plugin on the public NPM registry? (y/N)". Based on this input, the generator modifies the package.json. By default (No), it adds the "private": true flag and appends && exit 1 to the prepublishOnly script to firmly block public leaks. If the user opts in (Yes), it omits the private flag and the exit code, allowing the Joplin submission to succeed right before the standard NPM public upload continues.
We don't touch the existing prepare hook as it compiles the .jpl file and will be used for the build later.
3. Security Scanning Pipeline & Tooling Trade-offs
Once the submission is routed to a GitHub Issue, an event-driven GitHub Actions pipeline instantly wakes up to analyze the developer's repository. Rather than relying on a single security tool, I am proposing a dual-scanner approach to cover both static analysis and supply chain vulnerabilities.
| Tool | Main Strength | Pros | Cons |
|---|---|---|---|
| Semgrep | Custom code scanning | Fast, easy to maintain custom rules, lightweight CI usage | Less deep analysis than CodeQL |
| CodeQL | Deep security analysis | Very powerful multi-file vulnerability detection | Slow CI runtime, difficult rule language |
| Socket.dev | Supply chain security | Detects malicious packages, typosquatting, install-script abuse | Focused mostly on dependencies |
| Dependabot | Dependency maintenance | Built into GitHub, automatic dependency update PRs | Mainly checks known CVEs |
| Snyk | Vulnerability database + SAST | Strong dependency and code scanning coverage | Can become noisy/heavy for this use-case |
For Static Application Security Testing , the pipeline will utilize Semgrep.
While tools like CodeQL offer deeper semantic analysis it takes a lot of time to scan (often 10+ min), we need something that is fast and easy to maintain in future too.
Semgrep is chosen here because its syntax for writing rules is very high level and closely mirrors standard source code. This makes it significantly faster to run and much easier for the Joplin core team to write and maintain custom security rules specific to the Joplin API.
Because Semgrep does not deeply analyze the nested node_modules tree, the pipeline will pair it with Socket.dev and npm audit to specifically monitor the supply chain for hijacked dependencies, network access anomalies, or zero-day malware.
Tools like Dependabot mainly focuses on automated dependency updates, while Snyk heavily uses vulnerability databases and dependency analysis (almost same as npm audit). Hence, Socket.dev comes out to be the best fit for this situation.
The outputs of these scanners are aggregated into a single, readable Markdown report on the GitHub Issue, drastically reducing the cognitive load on the human maintainer during review.
| Tool | OSS Pricing / Limits | Notes |
|---|---|---|
| Semgrep | Free for public/open-source repositories | Very cost effective for custom rule-based scanning |
| CodeQL | Free for public GitHub repositories | No direct cost, but heavier CI runtime cost |
| Socket.dev | Free OSS tier | Good balance between supply-chain coverage and cost |
| Dependabot | Fully free on GitHub | Zero setup or infrastructure cost |
| Snyk | Free for public OSS repositories | Powerful, but larger projects may eventually require paid tiers |
YAML-Based Issue Forms To avoid the fragility of parsing free-form Markdown, the submission CLI will utilize GitHub Issue Templates. This allows the CI to extract metadata (Repository URL, Commit Hash, Plugin ID) as structured JSON objects, ensuring 100% parsing accuracy and preventing "malformed issue" errors.
Update Lifecycle :
When a developer submits a version update, the pipeline performs a Comprehensive Scan of the full codebase at the new commit. While the automated report highlights the Differential Changes (the delta between the approved_commit and the new submission) as a convenience to the reviewer, the security scan itself is always executed across the entire repository. This ensures that cross-file vulnerabilities... where malicious logic is split between previously approved code and new updates are fully detected. By surfacing the delta separately, the load on the human maintainer is drastically reduced, allowing for a rapid, focused review of routine version bumps.
4. Approval, Registry Mutation, and UI Integration
Automated scanning alone cannot prevent targeted attacks if a bad actor updates their logic to bypass static analysis. Therefore, human review remains the final gatekeeper.
Once a Joplin maintainer reviews the automated report and comments /approve on the Issue, a GitHub Action is triggered. To maintain strict security perimeters, this process uses a split-job architecture :
-
The first job safely builds the
.jplfile in an isolated environment. Runs in an environment restricted tocontents: read. It executesnpm ci --ignore-scriptsto prevent maliciouspostinstallscripts from compromising the runner. It builds the.jpland uploads it as a temporary internal artifact. -
The second job holds
contents: writepermissions. This job never executes developer code. It simply downloads the static artifact from Job 1 and uploads the artifact to GitHub Releases and updates the centralmanifests.jsonregistry via the GitHub REST API.
This API-driven mutation is faster, safer, and less prone to merge conflicts than a full Git clone-and-push cycle.
This split-job design creates a clear separation between building untrusted plugin code and modifying the official registry. The build step runs with restricted read-only permissions, while the publishing step has write access but only handles builded static artifacts. This reduces the risk of a compromised build environment affecting the Joplin registry itself.
Authorization & Race Conditions : The /approve trigger is validated by checking the author_association property of the GitHub Issue comment. The workflow only proceeds if the commenter is an official OWNER or MEMBER of the Joplin organization. Furthermore, to prevent corrupted states when multiple maintainers approve plugins simultaneously, the mutation job utilizes GitHub Concurrency Groups to strictly queue all writes to the manifests.json file.
Finally, to make this security model meaningful to the end-user, the Joplin Desktop React/Electron UI will be updated. Plugins marked as reviewed in the registry will display a "Verified Shield" badge within the app, while unverified legacy plugins will feature a subtle warning tooltip. This empowers users to make informed decisions about the code they install.
In the event of a post-approval security breach, maintainers can update a plugin's status to suspended in the registry. The Joplin UI will react by hiding these plugins from the store and automatically disabling existing installations to protect the user's local environment.
Suspending a compromised plugin, will be handled via a workflow_dispatch GitHub Action, providing maintainers with a safe, UI-driven way to mutate the registry without risking manual JSON syntax errors.
5. Legacy Plugin Migration Strategy
With hundreds of active plugins, breaking existing workflows or delisting developers is not an option. Existing plugins will be kept into the registry with an unreviewed status. They will function normally but will not receive the Verified Badge in the UI.
To upgrade, a legacy developer updates their project using the latest generator, increments the version number in package.json, and runs npm publish. This automatically drops their code into the new CI review pipeline, allowing them to earn the Verified Badge upon approval. Once the new pipeline demonstrates stability, the legacy cron job pulling blindly from NPM will be deprecated and removed entirely.

