developing a plugin - degoya/rDownloader GitHub Wiki

Developing a plugin

rdownloader plugin new scaffolds a plugin that builds, packages and passes conformance before you change a line of it โ€” so the first failure you see is about your code, not about the setup.

What you need

Tool Version
Rust 1.99, Edition 2024
Target wasm32-unknown-unknown (rustup target add)
wasm-tools 1.261.0 โ€” the version the bundled plugins are made with; it makes the component and checks its imports

Scaffolding

rdownloader plugin new --type resolver --out ./myhoster
cd myhoster
cargo build --release --target wasm32-unknown-unknown
wasm-tools component new target/wasm32-unknown-unknown/release/myhoster.wasm -o target/myhoster.wasm

--type takes every world of the contract, thirteen in all, and each has a page of its own under Plugin development that goes from the scaffold to a published package:

--type What it does
resolver Turns a hoster link into a downloadable address and checks links
transfer Carries the bytes of a protocol the application does not know
intake Turns text and URLs into LinkGrabber candidates
intake-mirrors An intake parser that also states every source of a file
crawler Turns one address into the files behind it
remote-job Runs a job that lives at a provider and outlives the call
auth Signs an account in through the provider's own flow
oauth Signs an account in by OAuth and renews the token
enricher Adds metadata to a link
notifier Delivers notifications to one more destination
postprocess Runs one more step after a package has been downloaded
storage Uploads finished packages somewhere
stream-transform Answers with an address and how its bytes become a file

The directory name becomes the crate name and the manifest's provider slug; --name sets a different one.

plugin new reads its templates from sdk/templates, so run it from a checkout of the repository or from a directory holding a copy of that folder. A release binary on its own does not carry them.

The scaffold brings its own copy of the WIT contract, rdownloader:[email protected], in wit/, so nothing about the build needs a checkout of rDownloader, and its own Ed25519 key pair in plugin-signing.key โ€” which belongs in your secret store, not in version control. plugin new writes a .gitignore before the key, listing plugin-signing.key, target/ and *.rdplug, so a first git add . leaves the key out; keep those lines when you edit the file.

Building without WASI

cargo build --release --target wasm32-unknown-unknown
wasm-tools component new target/wasm32-unknown-unknown/release/myhoster.wasm -o target/myhoster.wasm

cargo build makes a WebAssembly core module; wit_bindgen::generate! in src/guest.rs has embedded the world it implements, so wasm-tools component new needs neither a world nor a WASI adapter to turn it into the component, and the finished plugin imports only the WIT host contract.

A component that imports anything outside rdownloader:plugin is refused โ€” when it is packaged, and again when it is installed. That includes a wasi: interface pulled in by a dependency you did not choose directly.

scripts/check-plugin-imports.sh reads the import section with wasm-tools and names the offending interface. The CI workflow template runs the same check.

Capabilities

Each grant in [capabilities] is one WIT interface.

  • A component importing an interface its manifest does not grant is refused at packaging time and again at install time, so asking for less than you use fails at build time, not at a user's download.
  • Ask for exactly what you use. The plugin manager shows your grants to the person deciding whether to install you.
  • The file system, other processes, and the network beyond your declared domains are not capabilities that happen to be denied. There is no interface for them.

Translations

The scaffold ships locales/en.json. English is the one language a localised plugin must carry โ€” a package with any locale file and no en.json is refused.

The interface is offered in German, English, Spanish and French, and the bundled plugins ship all four as de.json, en.json, es.json and fr.json. A language a plugin lacks falls back to English.

Every scaffold is a working example

Each scaffold is a small, working plugin of its type rather than a set of signatures: a PIN sign-in, a resumable chunked upload, a CRC-32 over a package, a bounded folder walk, a key read out of a link's fragment. The promises the host makes to the type are written at the top of src/lib.rs; they are what shapes the code, so read them before changing it.

Everything that can run without a WebAssembly toolchain lives outside the component, and src/guest.rs โ€” the translation into the contract's types โ€” exists only on wasm32, so cargo test runs a fresh scaffold's unit tests straight away:

rdownloader plugin new --type oauth --out ./myprovider
cd myprovider && cargo test

Every scaffold carries a README.md naming its files and the rules worth knowing first. The templates themselves are in sdk/templates.

Conformance

rdownloader plugin conformance myhoster.rdplug --json

Answers machine-readably whether this application would run your package. Run it before you publish anything. Besides what installation checks, it asks a resolver or a crawler whether it claims links on hosts it does not declare, and checks that each German, Spanish or French catalogue you ship carries every code your English one does โ€” a language you do not ship falls back to English.

Every other type answers one question of its own, without a network: the run has none, and every request your plugin makes is refused. Each question is a check with a stable id in the --json report, so a CI job can act on the one that failed. A crash (a trap) never counts as a refusal.

Type Check id What is asked
notifier notifier_empty_destination A delivery to an empty destination is refused, not reported as delivered
auth auth_no_credentials A sign-in begun without credentials authorizes no account
intake intake_unknown_input Text in no format the parser reads yields no candidate
transfer transfer_undeclared_scheme An address in a scheme the manifest does not declare is refused
storage storage_unreached_verify verify never confirms an upload it could not reach
remote-job remote_job_containers A .torrent and an .nzb are claimed exactly when [extension] containers declares them
enricher enricher_empty_input An empty link gets no fields, and no failure
postprocess postprocess_empty_input A package without files finishes or is skipped, with no failure and no warning
stream-transform stream_transform_empty_input The empty address is not claimed, and asking does not fail

An OAuth plugin is instantiated against its world; it has no question of its own yet.

Limits

[limits] in your manifest names only what you change. Each field you leave out, or the whole table, is the host's default:

Field Default Most a manifest may declare
memory_bytes 64 MiB 512 MiB
fuel 2 000 000 000 40 000 000 000
timeout_milliseconds 15 000 (15 s) 1 800 000 (30 min)
max_response_bytes 8 MiB 64 MiB
wait_budget_milliseconds 600 000 (10 min) 1 800 000 (30 min)

A value above the last column is refused when the plugin is packaged.

Every other field of your manifest stays yours to state: the identity, the signing key and [metadata]. The bundled plugins share theirs through a file of their own repository, which plugin package --defaults writes into the packaged copy; a plugin of your own needs no such file.

Errors and user-facing text

User-facing text never lives in the plugin as prose. It lives as a stable code, translated in the plugin's own catalogue โ€” the same rule the application follows.

An error value a provider wrote that is not code-shaped is dropped whole rather than filtered: filtering keeps the digits of a leaked credential.

Read an HTTP status nothing else explains the way the bundled plugins do, so a person sees the same outcome whichever plugin served the link:

Status Category Why
401, 403 account-invalid โ€” or a plain HTTP error (permanent) when the request carried no account A bot wall must not send anybody to fix credentials they never gave
404, 410 permanent The file is gone for good; it is not retried. A link check still reports it offline
451 offline Withheld for legal reasons, often regionally; it is retried
429 rate-limited, with the provider's Retry-After
5xx transient, with the provider's Retry-After where one was sent

Take Retry-After in seconds, at most one day โ€” the host holds every wait to a day anyway โ€” and read Retry-After: 0 as your own default wait, not as "retry at once".

Testing against a running service

rdownloader serve --plugin-development-mode

accepts unsigned packages. Package with --development to produce one. Local development only.

Related pages

Reference in the repository: sdk/README.md ยท rdownloader.wit, the contract

โš ๏ธ **GitHub.com Fallback** โš ๏ธ