running a plugin repository - degoya/rDownloader GitHub Wiki
A plugin repository is how people get your plugins β and every later version of them β without downloading files by hand. They add it once under Settings β Plugins β Repositories β Plugin repositories with its address and your repository public key; from then on your plugins appear in the Catalogue tab's Services and plugins, and your new versions there as Update available. This page is for whoever publishes one; how a single package is built and signed is in Packaging, signing and distribution.
A repository is a static directory on an HTTPS host: your signed .rdplug packages and one
signed document, rdownloader-plugin-index.json, that lists them. Nothing runs on the server.
- It delivers; it vouches for nothing. Every package it lists is installed exactly like an uploaded one: it must be signed by a plugin key the person trusts, and the first package under an unknown plugin key asks them to compare its fingerprint. Approving your repository key does not approve your plugin key, and the other way round.
- It describes every package precisely. The index names each package by the digest its plugin signature covers, its size, id, version, type, contract, publisher and permissions. A downloaded package that differs from its entry in any of these is refused before it is installed.
- An update is the same publisher's newer version. A version is offered as an update only when it is signed by the plugin key the installed version is signed with. A newer version under another key appears as a separate package with its own review.
- It can withdraw only what it delivered. A third-party repository withdraws package versions that were installed from it on that installation, and nothing else β no key, no bundled plugin, no version someone uploaded by hand.
- It expires. Every index carries a validity window. An expired index is refused, so a repository needs a newly signed index at regular intervals, even when nothing changed.
One repository can list packages from several authors; each author's plugin key is still trusted on its own.
A repository uses two kinds of key, one per role, and a key of one role signs nothing under the other:
| Key | Signs | Who holds it |
|---|---|---|
| Plugin key | each .rdplug
|
the plugin's author |
| Repository key | the index | whoever runs the repository |
Generate the repository key once:
rdownloader plugin keygen --role repository --output ./keysThis writes rdownloader-repository.key (the private key, PEM, readable only by you) and
rdownloader-repository.pub (the public key, Base64). The key id it prints,
rdownloader-repository-v1, is the official repository's; choose your own instead, for example
example-org-repository-v1, and pass it as --key-id every time you sign. People approve your
key under the id your index names, so keep both for as long as the repository exists.
-
Keep the private key offline or in a CI secret, never in the repository and never on the web host. Whoever holds it can sign an index every installation of your repository accepts.
-
Publish the fingerprint somewhere other than the web host of the index β the README of your source repository, your website. It is the hexadecimal SHA-256 of the public key:
base64 -d < keys/rdownloader-repository.pub | sha256sum
Adding the repository shows this fingerprint and asks the person to compare it. That comparison is the whole protection against someone serving an index under their own key.
-
Publish the public key (the one line in
rdownloader-repository.pub) and the index address beside it. The address is the full URL of the index file, not of its directory.
Any static HTTPS host works: GitHub Pages, an object storage bucket with a public HTTPS endpoint, a plain web server. What the application requires of it:
-
HTTPS at every step. The index address is
https://without a user name or password; plain HTTP is refused, and so is a redirect to it. At most five redirects are followed. - Plain bytes. The file is read as it is served; the content type does not matter.
- Answers in time. Each fetch β the index or one package β has 120 seconds.
- Bounded sizes. The signed index at most 4 MiB, a package at most 96 MiB and never more than the size its entry declares.
A directory that serves the packages beside the index:
https://example.org/rdownloader/
βββ rdownloader-plugin-index.json
βββ myhoster-1.0.0.rdplug
βββ myhoster-1.1.0.rdplug
βββ mycrawler-0.2.0.rdplug
Package addresses. By default each entry's address is the package's file name, resolved
against the index's own address β right when the packages sit beside the index, and it keeps the
directory movable. A relative address cannot leave the index's directory: no leading /, no
.., no scheme, query or fragment, and no spaces, so name the files plainly
(<name>-<version>.rdplug). When the packages live elsewhere, --base-url writes absolute
addresses instead; it has to be https:// and end with /, and every package must then be found
directly under it.
-
Put the signed packages into one directory β one
.rdplugper plugin version, each already checked withrdownloader plugin verifyandplugin conformance. -
Build and sign the index:
rdownloader plugin index build ./site --out ./site/rdownloader-plugin-index.json \ --key ./keys/rdownloader-repository.key --key-id example-org-repository-v1
--keycan be left out whenRDOWNLOADER_REPOSITORY_SIGNING_KEYholds the PEM text. The warning that the application embeds no repository root under your key id is expected. The command refuses to sign an index the application would refuse, and prints how many packages it signed and until when the index is valid. -
Upload the directory to the host.
-
Add the repository on a test installation (see Testing) β
rdownloader plugin index verifychecks only the official repository's index. -
Publish the index address, the public key and the fingerprint.
The SDK ships a workflow that does all of this:
sdk/ci/repository.yml.
Copy it into .github/workflows/ of the repository that publishes your packages, then:
- Store the PEM text of the repository key as the secret
REPOSITORY_SIGNING_KEY. - Set
KEY_IDin the workflow to your key id. - Choose
PACKAGE_SOURCE:releasescollects every.rdplugattached to a published, non-prerelease release of the repository;foldertakespackages/. - Set Settings β Pages β Source to GitHub Actions.
It runs when a release is published, when packages/, notes/ or revocations.json change on
main, once a month, and by hand. Each run downloads the rDownloader release named in
RDOWNLOADER_VERSION and checks it against that release's SHA256SUMS, builds and signs the
index with relative addresses, and deploys the
packages and the index to Pages. The index address is then
https://<owner>.github.io/<repository>/rdownloader-plugin-index.json. The key reaches the
build step only through the secret; it never appears in the log.
GitHub switches off scheduled workflows in a public repository that has had no activity for 60 days, and mails the owner before it does. Switch the workflow back on when that happens, or the index runs out.
Add the new package to the directory, build the index again over all packages, and upload both. Nothing else is needed:
-
The sequence takes care of itself. An installation refuses an index whose sequence is not
higher than the last one it accepted, so an older index cannot be replayed;
index builduses the signing time in seconds. Pass--sequenceonly with a reason: after an index with a very high sequence, every installation that saw it refuses all lower ones. - Installations see it within a day. Each installation checks its repositories shortly after it starts and then every 24 hours by default (between 1 and 168, set by the person), or when the person clicks Check now.
- Keep older versions listed while someone may need them. For each installed plugin, the update offered is the newest version that installation can run: an entry whose minimum rDownloader version or contract is too new for it is listed as incompatible, and the newest compatible one is offered instead. Listing only the newest version leaves an installation that cannot run it without an update. Older entries also keep their release notes in the plugin's version history. Dropping a version from the index removes nothing that is installed; the index holds at most 512 packages.
-
Never republish a version with other content. A version number stands for one package. A
changed build gets a new version, raised in its
manifest.toml. - Updates wait for a click by default. A person can let a plugin update itself; even then an update that asks for a permission the installed version does not have waits for review. Whatever is installed becomes active after the next restart.
--notes <dir> reads one plain-text file per package, named after the package file:
myhoster-1.1.0.rdplug takes myhoster-1.1.0.txt. A package without a file has no notes. Each
note is at most 2,000 characters of plain text; newlines and tabs are kept, other control
characters refuse the index, and nothing is rendered as markup. The notes appear in the install
preview, beside an update and in the plugin's version history, where a note longer than three
lines is folded behind More. A file that says nothing but Maintenance release: internal
changes only, no change in behaviour. gives its package no notes, and rDownloader shows no
notes that say only that, so the plugin card never reads "Maintenance release"; notes several
versions repeat word for word show once, at the newest. The SDK workflow passes notes/ when it
exists. Each scaffold
carries a CHANGES.md with a ## <version> section per version: one to three sentences on what
someone who installs the plugin notices is what the official plugins publish, and it reads best.
A version that must not be installed any more β a broken or dangerous build β is withdrawn by its package digest, not by deleting it:
-
Look up the digest in your current index:
jq -r '.payload.packages[] | select(.version == "1.1.0") | "\(.name) \(.package_digest)"' \ rdownloader-plugin-index.json -
Write it into the withdrawal file, exactly in this shape (both lists may be empty, no other field is accepted; each digest is 64 lowercase hexadecimal characters):
{ "package_digests": [ "3f5cβ¦64 lowercase hexβ¦" ], "keys": [] } -
Remove the package itself from the directory (with the SDK workflow: delete the asset from its release, or the file from
packages/). An index that offers a package it withdraws is refused as contradictory, andindex buildstops with "is offered and withdrawn". -
Build with
--revocations revocations.jsonand publish. The SDK workflow passesrevocations.jsonwhen it exists.
What installations do: at the next check, an installation that installed this version from your repository records the withdrawal. The version no longer loads from the next start; a download pinned to it is held with the reason shown in the queue, and resuming it runs the plugin's current version. Nothing already running is stopped. Publish the fixed version as a new version number in the same index, so the update is waiting.
- Keep every withdrawn digest in the file for good. An installation that was offline picks the withdrawal up from whichever index it sees next, and removing a digest from the file lifts nothing that was already recorded.
- The
keyslist is read only from the official repository. A third-party repository cannot withdraw a plugin key; the plugin's author rotates it as described under Rotation and withdrawal. - A version someone uploaded by hand, or installed from another repository, is out of your repository's reach. Tell users; each person can withdraw a version on their own installation.
Every index has an issue time and an expiry. index build makes it valid for 90 days
(--valid-days, between 1 and 366). After the expiry:
- a check refuses the index as
plugin_index.stale, and the repository shows that error; - at the next start the installation deletes its cached copy, and the repository offers nothing and no update until a valid index is published;
- everything already installed stays installed and keeps working.
Re-sign on a schedule, well inside the window β the SDK workflow does it monthly. A re-signed index with unchanged packages is a new index with a higher sequence, and installations take it as such. The issue time must not be more than an hour ahead of the installation's clock, so sign on a machine whose clock is right.
There is no rotation mechanism for a repository key. An installation stores the key it approved together with its key id; an index signed by another key or under another id is refused, and the only way to a new key is for each person to remove the repository and add it again with the new key.
- Planned change. Publish the new public key and fingerprint, and serve an index signed with the new key at a new address, while the old address keeps being re-signed with the old key until people have moved; then stop the old one. Removing a repository forgets which versions were installed through it, so a withdrawal from the new repository does not reach versions installed through the old one.
- Lost key. No new index can be signed. The last one expires within its window, and the repository then offers nothing. Generate a new key, publish it with its fingerprint, and ask people to remove the repository and add it again. Installed plugins are not affected.
- Leaked key. Whoever holds it can sign indexes that installations accept. It cannot make them install a package β that still needs a trusted plugin key β but it can hide your versions and withdraw versions installed from your repository. Ask people to remove the repository, then proceed as for a lost key. There is no way to revoke a third-party repository key remotely.
A lost or leaked plugin key is a different matter: Key management.
Use an installation that is not your everyday one β the release binary started in an empty directory, with its own port:
mkdir rd-test && cd rd-test
rdownloader serve --listen 127.0.0.1:8711The index has to be on a real HTTPS host; the application refuses anything else, including a local plain-HTTP server. Publish it at its final address or at a staging copy, then:
- Settings β Plugins β Repositories β Plugin repositories β Add a repository: the index address and the Base64 public key.
- Compare the fingerprint the dialog shows with yours, and approve.
- Check that the Catalogue tab lists every package as Repository, with its release notes, and that Index valid until shows the expected date.
- Install one plugin, restart, and check that it runs.
- Publish a newer version, click Check now, and check that it appears under Updates in the Catalogue.
When a check fails, the repository in Settings β Plugins β Repositories shows Last check failed with the reason. The index accepted before stays in use until it expires, so a broken publication does not remove anything at once.
| Code | Cause | Fix |
|---|---|---|
plugin_repository.url_invalid |
The address is not a plain https:// URL with a host, or it carries a user name, a password or a fragment. |
Give the full https:// address of the index file. |
plugin_repository.key_invalid |
The pasted key is not a Base64 Ed25519 public key. | Paste the one line from rdownloader-repository.pub. |
plugin_repository.key_does_not_sign |
The pasted key signed nothing in this index β another key, or the plugin key by mistake. | Publish the public key of the repository key the index is signed with. |
plugin_repository.key_fingerprint_mismatch |
The fingerprint confirmed differs from the key's. | Add the repository again and confirm the fingerprint it shows. |
plugin_repository.already_added |
This address is already in the list. | β |
plugin_repository.download_failed |
The host answered with an error (404 after a move), redirected to HTTP or more than five times, took longer than 120 seconds, or sent more than allowed. | Check the address in a browser; serve over HTTPS; keep the index under 4 MiB. |
plugin_index.malformed |
What came back is not a signed index β an HTML page, a truncated or edited file. | Serve the file exactly as index build wrote it. |
plugin_index.untrusted |
The index is signed under another key id than the approved one. | Sign with the same --key-id as before; a new id means removing and adding the repository. |
plugin_index.bad_signature |
The key id matches but the signature does not: another key under the same id, or a file changed after signing. | Sign with the approved key; never edit the signed file. |
plugin_index.stale |
The index expired, its sequence is not higher than one already accepted, or it is issued more than an hour in the future. | Re-sign with the default sequence on a machine with a correct clock. |
plugin_index.invalid |
The content breaks a rule: a limit, a duplicate id and version, an address that leaves the directory, a package that is offered and withdrawn. | Build with index build, which refuses the same things. |
plugin_index.too_large |
The signed index is over 4 MiB. | List fewer packages or shorter notes. |
plugin_index.schema_version_unsupported |
The index uses a format this rDownloader cannot read. | Build with a release whose format the installations read. |
plugin_index.revoked |
rDownloader itself withdrew this exact document. | Publish a newly signed index. |
plugin_repository.digest_mismatch |
The downloaded package is not the one the index names β replaced after the index was built, or a stale cache in front of the host. | Rebuild the index after every change to the packages, and never replace a published file. |
plugin_repository.package_mismatch |
The package's own manifest differs from its entry, or it is unsigned. | Build the index from the very packages you publish. |
What index build itself refuses:
| Message | Fix |
|---|---|
the key given is not the repository root rdownloader-repository-v1 |
Pass --key-id with your own key id. |
no .rdplug packages in β¦ |
Point it at the directory that holds the packages. |
β¦ is unsigned / β¦ is not signed by the key its manifest names
|
Package and sign with your plugin key, then verify. |
β¦ is listed twice |
Two files carry the same plugin and version; keep one. |
β¦ is offered and withdrawn |
Remove the withdrawn package from the directory. |
--valid-days must be between 1 and 366 |
Choose a window in that range. |
the base URL β¦ must end with '/' / is not https
|
Give --base-url as https://β¦/. |
- Packaging, signing and distribution β keys, packaging and the index flags
- Installing and trust β what the person adding your repository sees
- Plugin reference β the index format and its limits
- Developing a plugin