Skip to content

FluxDown 0.5.0 ships the native GPUI client — a note from the author on why the whole interface was rebuilt.Take a look

FluxDown
Download Docs Pricing Changelog Sponsor
Plugin Development Manifest Reference

Plugin Development

Manifest Reference

Every manifest.json field with its validation rules, the settings widget matrix, URL pattern rules, and how the manifest is checked at install and load time.

Edit this page

manifest.json sits at the root of the plugin package and declares what the plugin is, which entry scripts it ships, what it contributes to the UI, and which capabilities it needs. All keys are camelCase. Unknown keys are rejected — a typo fails the whole manifest instead of being silently ignored.

This page is the field-by-field reference. For a guided walkthrough see Your first plugin (hand-written JavaScript) or TypeScript project (SDK, build and pack). Permissions and consent are covered in Permissions, Consent & Audit, and packaging in Packaging & Plugin Market.

A complete example

{
  "identity": "acme@demo",
  "name": "Acme Demo",
  "version": "1.0.0",
  "description": "Resolves Acme share links and tidies finished files.",
  "homepage": "https://example.com/acme-demo",
  "icon": "icon.png",
  "minAppVersion": "0.9.0",
  "resolvers": [
    { "match": { "urls": ["*://acme.example/share/*"] }, "entry": "resolve.js", "timeoutMs": 15000, "multi": true }
  ],
  "hooks": {
    "entry": "hooks.js",
    "events": ["onDone", "onError", "onCancel"],
    "match": { "urls": ["*://acme.example/*"] }
  },
  "main": { "entry": "main.js", "events": ["onDone"], "background": false },
  "contributes": {
    "views": [{ "id": "dashboard", "title": "Dashboard", "location": "activity" }],
    "commands": [{ "id": "refresh", "title": "Refresh", "palette": true }]
  },
  "settings": [
    { "key": "quality", "title": "Preferred quality", "type": "string", "widget": "select",
      "options": [ { "value": "hd", "label": "HD" }, { "value": "sd", "label": "SD" } ], "default": "hd" }
  ],
  "permissions": []
}

A plugin may declare any combination of resolvers, subscriptions, hooks, interceptors, auth, main and contributes. A manifest that declares none of them is valid but does nothing.

Top-level fields

FieldRequiredTypeRules
identityyesstring^[a-z0-9_-]+@[a-z0-9_-]+$ — lowercase author, @, name. Dots are forbidden. Permanent ID: settings, storage, consent and the install folder are all keyed by it.
nameyesstringDisplay name; must not be empty or whitespace only.
versionyesstringThree integer segments MAJOR.MINOR.PATCH. See Version strings for how strictly this is parsed.
descriptionnostringShown in the plugin list.
homepagenostringProject or author URL.
iconnostringPath inside the package. No .., no absolute path, no drive letter, no leading / or \, no empty segments.
minAppVersionnostringMinimum FluxDown version, same parsing as version. See minAppVersion behaviour.
resolversnoarrayAt most one entry. See resolvers[0].
subscriptionsnoarrayAt most one entry. Subscription provider. See subscriptions[0].
hooksnoobjectDownload lifecycle callbacks. See hooks.
interceptorsnoobjectTask interceptors (request rewrite, segment filtering, post-processing). See interceptors.
authnoobjectPlatform login entry. See auth.
mainnoobjectAlways-on script for portable views, commands and background logic. See main.
contributesnoobjectViews, commands and browser sniff rules. See contributes.
permissionsnoarrayCapability grants; unknown values are rejected. See permissions.
settingsnoarrayDeclarative settings fields. See settings[].

Version strings

version and minAppVersion are compared as three integers; anything after the third segment’s number is ignored by the client.

WhereWhat is accepted
FluxDown client (install and load)MAJOR.MINOR.PATCH, optionally prefixed with v, optionally followed by -pre or +build (the suffix is cut off before comparing). 1.2.3, v1.2.3 and 1.2.3-rc.1 all parse as 1.2.3. Exactly three segments are required.
Plugin index and fluxdown-plugin validateStrict: exactly MAJOR.MINOR.PATCH, digits only, no leading zeros, no v prefix, no suffix.

Always write the strict form. A version the client tolerates can still be rejected when you publish to the market.

minAppVersion behaviour

If the running FluxDown is older than minAppVersion (or either side cannot be parsed), the plugin is not loaded. It appears in the plugin list with a failed load status and the reason 需要 App ≥ X,当前 Y (“requires App ≥ X, current is Y”) — it is not silently skipped. Installing such a package is rejected with 安装后未能加载插件(校验失败或版本门槛不满足) (“the plugin could not be loaded after install (validation failed or version gate not met)”) and rolled back.

Raise minAppVersion whenever you start using a field, event or permission that older versions do not know: an older client rejects an unknown key or permission outright as an invalid manifest, so the version gate is what produces a clear message instead.

resolvers[0]

A resolver turns a page URL into one or more downloadable files.

{
  "resolvers": [
    {
      "match": { "urls": ["*://host.com/share/*", "*://cdn.host.com/*"] },
      "entry": "resolver.js",
      "timeoutMs": 15000,
      "multi": false
    }
  ]
}
FieldRequiredRules
match.urlsyesNon-empty list of URL patterns (see URL pattern rules).
entryyesScript file; safe relative path. Must define globalThis.resolve.
timeoutMsnoPer-call timeout in milliseconds. Must not be 0. Replaces the 10 s default but is capped at the 30 s hard ceiling whatever you write.
multinoBoolean, default false. true declares that this resolver may return a multi-file manifest and should take part in pre-resolution (the preview that lists the files before a task is created). With false, a single-file resolver is not run ahead of time just to find out; if it returns a multi-file list when the task starts, the engine still splits it into separate tasks, so multi is a hint for pre-resolution and not a requirement for returning lists.

The resolve(ctx) contract and its return shapes are in the API reference.

subscriptions[0]

A subscription provider feeds the subscription feature from a source other than plain RSS — for example a site’s API that lists new episodes.

{
  "subscriptions": [
    { "providerId": "acme-feed", "entry": "subscribe.js", "timeoutMs": 20000 }
  ]
}
FieldRequiredRules
providerIdyes^[a-z0-9_-]{1,64}$. Stable: it is stored on subscription records. rss is reserved for the built-in provider and is rejected.
entryyesScript file; safe relative path. Must define globalThis.subscribe; the function name is fixed.
timeoutMsnoPer-call timeout in milliseconds. Must not be 0. Default 10 s; can be set up to the 30 s hard ceiling.

At most one provider per plugin. The subscribe(ctx) contract, the item shape and the validation applied to returned items are explained in the Subscription guide.

hooks

Hooks run your code on download lifecycle events.

{
  "hooks": {
    "entry": "hooks.js",
    "events": ["onStart", "onDone", "onError", "onCancel"],
    "match": { "urls": ["*://host.com/*"] }
  }
}
FieldRequiredRules
entryyesScript file; safe relative path. Must define a global function named after each subscribed event.
eventsyesNon-empty list; each item one of onStart, onError, onDone, onMetaProbed, onCancel. Anything else is rejected.
matchnoURL filter with the same pattern rules. If present, urls must be non-empty. Omitted = the hooks fire for every task.

Caveat: if the same plugin also declares a resolver, onMetaProbed never fires for its tasks — resolver tasks skip the metadata probe entirely. FluxDown logs a warning if you subscribe to it anyway.

Event payloads, requestRetry, ffmpeg/ffprobe and yt-dlp post-processing are covered in the Hooks guide.

interceptors

Interceptors let a plugin read and change a task while the engine runs it — request identity, concurrency, streaming segments, post-processing and retry decisions. At most one interceptors object per plugin. It requires the intercept permission (elevated risk, see Permissions).

{
  "permissions": ["intercept"],
  "interceptors": {
    "entry": "intercept.js",
    "stages": ["prepare", "segments"],
    "match": { "urls": ["https://example.com/*"] },
    "timeoutMs": 3000,
    "onFailure": "continue"
  }
}
FieldRequiredRules
entryyesScript file, safe relative path. Must define the single global intercept(ctx); it dispatches on ctx.stage.
stagesyesNon-empty, no duplicates. Each item one of prepare, probed, refresh, selectVariant, selectPeriod, segments, liveSegments, finalize, failed. An unknown name is rejected with the list of valid ones.
matchyesURL filter, same pattern rules; urls must be non-empty. A task is intercepted when a pattern matches its original URL or its resolved URL.
timeoutMsnoWall-clock limit of one call, must not be 0. Default 5 s, capped by the hard ceiling; liveSegments is further capped at 2 s and refresh at 5 s.
onFailurenocontinue (default) or fail. With continue, an error or invalid return value of this plugin is noted in the task activity and ignored; with fail, the task fails with [插件] <identity>(<stage>):<message>.

Validation runs after auth, in this order: entry path → stages non-empty → every stage known → no duplicate stage → match.urls non-empty → timeoutMs ≠ 0 → permissions includes intercept. The entry file must exist in the package and is read at load time like hooks.entry. Stage contexts, return values, the RequestPatch rules and defineInterceptors are in the Interceptors guide.

auth

{
  "auth": { "entry": "auth.js", "timeoutMs": 20000 }
}
FieldRequiredRules
entryyesScript file; safe relative path. Must define globalThis.authenticate.
timeoutMsnoPer-call timeout in milliseconds for authenticate. Must not be 0. Replaces the 30 s default but is capped at the 30 s hard ceiling; independent from resolvers[0].timeoutMs.

permissions must include "auth" when auth.entry is declared, otherwise validation fails. The five login actions and the full flux.auth surface are in the Auth guide and the API reference.

main

{ "main": { "entry": "main.js", "events": ["onDone"], "background": false } }
FieldRequiredRules
entryyesClassic script (not an ES module); safe relative path inside the package, extension .js, file must exist.
eventsnoEvents delivered to the script; each must be one of onStart, onError, onDone, onMetaProbed, onCancel.
backgroundnoBoolean. true keeps the script running with the daemon. Requires the background permission.

main is the always-on part of a plugin: portable views, commands and background logic run here, inside the daemon. See the UI guide.

contributes

{
  "contributes": {
    "views": [{ "id": "dashboard", "title": "Dashboard", "location": "activity",
                "desktop": { "entry": "ui/dashboard.js", "networkHosts": ["api.example.com"] },
                "portable": true }],
    "commands": [{ "id": "refresh", "title": "Refresh", "palette": true, "taskMenu": false }],
    "sniffRules": "rules.json"
  }
}

Empty views / commands arrays are the same as leaving them out.

views[]

At most 16.

FieldRequiredRules
idyesMatches ^[a-z][a-z0-9-]{0,31}$; unique among views.
titleyes1–40 characters.
locationyesactivity, settings, taskDetail or window. At most one view may use settings.
desktopnoNative desktop implementation, see below. A view without desktop is portable and requires main.
portablenoBoolean, default false. Only allowed together with desktop. true means main also implements this view (via flux.ui.view) so clients that cannot run desktop scripts can show it; requires main.

Where a view is shown depends on its declaration:

DeclarationDesktop (GPUI)WebMobile
no desktop (portable)rendered from mainrendered from mainnothing is rendered
desktop + portable: truenative desktop script; falls back to the main implementation if the script package is not readable locallyrendered from mainnothing is rendered
desktop onlynative desktop script; when the plugin’s script package is not readable on this machine (for example the daemon runs on a remote host) the GPUI client shows a desktop-only noticenot shown at all — the Web UI lists only portable views and never shows a “desktop only” cardnothing is rendered

Use portable: true when you want the same view on Web, and keep the desktop script as an enhancement. Details of the node tree and events are in the Views reference and the UI guide; native scripts are covered by the Desktop UI guide.

views[].desktop

FieldRequiredRules
entryyesUI script; safe relative path inside the package. Extension must be .js or .mjs, and the file must exist in the package.
width / heightnoInitial window size in logical pixels. Only allowed when the view’s location is window. Finite number within [240, 4096].
networkHostsnoHosts the UI script may connect to. At most 64 entries per view; each must be a lowercase DNS hostname (RFC 1123 labels, at most 253 characters) or an IP literal — no scheme, port, path or wildcard — and unique within the view.

Any view with desktop requires the desktop permission. desktop.entry is an ES module executed by the GPUI Shell built into the GPUI desktop client (it may import from "gpui-kit", "gpui-base" and "gpui-component"). The engine and daemon never compile or execute it; they only validate the path and check that the file exists. Web and mobile clients do not run it.

networkHosts is an enforced allow-list: the script’s network access is limited to exactly these hostnames. File system, process and clipboard access are not restricted — the script runs with full trust in the desktop process, so install only plugins you trust. See Permissions. The union of all views’ networkHosts is shown in the consent dialog and is part of what you approve.

commands[]

At most 32; requires main.

FieldRequiredRules
idyesSame pattern as view ids; unique among commands.
titleyes1–40 characters.
palettenoBoolean. Show in the command palette.
taskMenunoBoolean. Show in the task context menu.

sniffRules

Safe relative path to a .json rule pack for the browser extension. Requires the browserRules permission.

  • The file must exist in the package, be at most 256 KiB, parse as JSON, contain "format": "fluxdown-sniff-pack" and a rules array of at most 500 entries.
  • The format of each rule is the one documented in the Sniffing rules reference.

A minimal rules.json:

{
  "format": "fluxdown-sniff-pack",
  "packId": "acme-demo",
  "sequence": 1,
  "engine": 1,
  "issuedAt": "2026-10-17T00:00:00Z",
  "rules": [
    {
      "id": "video",
      "version": 1,
      "tier": "plugin",
      "stage": "response",
      "match": { "host": ["acme.example"], "ext": ["xyz"] },
      "action": { "kind": "resource", "type": "video" }
    }
  ]
}

How the rules land in the browser extension:

  • Only rules from plugins that are enabled, loaded and consented are served. Disabling the plugin, or leaving a new permission unapproved, removes its rules.
  • Each rule id is rewritten to plugin.<identity with @ replaced by .>.<your id> — the rule above becomes plugin.acme.demo.video. If the rewritten id is longer than 64 characters, the rule is dropped (and logged).
  • tier is forced to plugin whatever you write. Rules that are not objects or have no string id are dropped.
  • Plugin rules rank below local and official rules and above community and built-in ones.

permissions

Extra host capabilities a plugin opts into. Empty or omitted = the base sandbox (network via flux.fetch, flux.storage, logging, view and command contributions). Recognised values:

ValueGrants
ffmpegThe flux.ffmpeg / flux.ffprobe APIs — run the resolved ffmpeg on a finished file.
ytdlpThe flux.ytdlp API — run the resolved yt-dlp from resolve or any hook.
authDeclare auth.entry for a platform login flow, and allow flux.fetch / flux.auth to use stored credentials.
tasksflux.tasks.* — operate on download tasks from main.
browserRulesContribute browser-extension sniff rules via contributes.sniffRules.
desktopRun desktop scripts (contributes.views[].desktop) with full trust in the desktop process.
backgroundKeep main running with the daemon (main.background).

Unknown values fail the whole manifest. Risk levels, consent and audit are explained in Permissions, Consent & Audit; the API behind each permission is in the API reference.

Permissions that a declaration depends on:

DeclarationRequires
auth.entryauth
main.background: truebackground
any views[].desktopdesktop
contributes.sniffRulesbrowserRules

(tasks, ffmpeg and ytdlp are checked when the API is called, not by the manifest.)

settings[]

Each entry describes one field; the app generates the form on desktop and Web. Keys must be unique and non-empty.

{
  "settings": [
    { "key": "apiToken", "title": "API token", "type": "string", "widget": "password", "required": true },
    { "key": "quality",  "title": "Preferred quality", "type": "string", "widget": "select",
      "options": [ { "value": "hd", "label": "HD" }, { "value": "sd", "label": "SD" } ],
      "default": "hd" },
    { "key": "maxRetries", "title": "Max retries", "type": "number", "min": 0, "max": 10, "default": "3" },
    { "key": "verbose", "title": "Verbose logging", "type": "boolean", "default": "false" }
  ]
}
FieldRequiredNotes
keyyesUnique within the plugin, not empty. Read back as flux.settings.<key>.
titleyesForm label.
descriptionnoHelp text below the field.
typeyesstring, number, or boolean.
widgetnoDefaults by type: string→text, number→number, boolean→toggle.
optionsselect onlyNon-empty list of { "value", "label" }.
defaultnoAlways a string, even for numbers ("3") and booleans ("true" / "false"). For select, must be one of the option values; for number, must parse as a finite number and fall inside min / max.
requirednoBoolean. The form refuses to save an empty value.
min / maxnumber onlyFinite, inclusive bounds, min ≤ max. Rejected on other types.
patternstring onlyA JavaScript RegExp (not a Rust regex) validated against the value on save. It is compiled at install time; an invalid pattern rejects the install.
helperScriptstring onlyA snippet the host shows a Copy button for, next to the field. The host only copies the text to the clipboard — it never runs it. The user pastes it into the target site’s browser DevTools console (typical use: read document.cookie and copy a login cookie back into the field). Non-empty, at most 4 KB.
helperLabelwith helperScriptButton label, at most 60 characters. Only valid together with helperScript; a generic “Copy helper script” label is used when omitted.

Widget × type matrix

Only these combinations are valid — anything else fails validation:

WidgetAllowed type
text, password, textarea, folder, selectstring
toggleboolean
numbernumber

URL pattern rules

Patterns are not regular expressions and not browser match patterns. The rules, in full:

  • * is the only wildcard and matches any run of characters (including / and :).
  • Text between *s must appear in order as substrings.
  • If the pattern doesn’t start with *, the first segment must be a prefix of the URL.
  • If it doesn’t end with *, the last segment must be a suffix.
  • Matching is case-insensitive (both sides are lowercased whole, path included).
  • A bare * matches everything.

Examples against https://www.youtube.com/watch?v=abc:

PatternResult
*://www.youtube.com/watch*matches
https://x.com/a (no wildcard)exact match only
*://x.com/* vs https://y.com/ano match — ://x.com/ not found
youtube.com/*no match — the pattern is prefix-anchored and the URL starts with https://

Validation

The manifest is validated when the plugin is installed and every time it is loaded. A plugin that fails validation does not run; it is listed as failed with the reason, and the reason is written to the log (see Debugging).

Order of checks

identity format → name non-empty → version format → minAppVersion format → icon path safety → at most one resolver → resolver entry path, non-empty match.urls, timeoutMs ≠ 0 → at most one subscription → providerId format and not rss, entry path, timeoutMs ≠ 0 → hooks entry path, non-empty valid events, non-empty match.urls if present → auth entry path, auth permission present, timeoutMs ≠ 0 → main (path, .js, valid events, background permission) → contributes (view and command limits and ids, settings location at most once, portable only with desktop, main required for portable views and commands, desktop entry and size and hosts rules, desktop permission, sniffRules path / extension / browserRules permission) → settings (keys unique, per-field rules and widget × type matrix) → every permissions value known.

Files that must exist

At install and load time the package must contain every declared entry script (resolvers[0].entry, hooks.entry, auth.entry, subscriptions[0].entry, main.entry), every desktop.entry, and the sniffRules file (which must also pass the content check above). A missing file is reported as 包内缺少文件 '<path>' (“the package is missing file ''”); an entry script that exists but cannot be read as UTF-8 text is reported as 读取入口文件 '<path>' 失败: … (“failed to read entry file”). This applies to dev-mode plugins too: the plugin is listed as Load failed instead of failing later, on the first call.

Compile checks at install

Syntax errors are rejected at install time rather than discovered on the first download. The compile check covers the resolver, hooks, auth and subscription entry scripts (up to four, one per declared kind). It does not cover main (compiled on demand) or desktop scripts (never compiled by the engine). Each settings[].pattern is also compiled as a JavaScript RegExp at install time; an invalid pattern rejects the install.

A failed install changes nothing: a first install leaves no folder behind, and an update restores the previous version.

Checking before you ship

fluxdown-plugin validate runs the same rules plus the stricter listing checks (strict version format, every entry and the icon present in the package, entry and size limits). See the SDK reference and, for validating by hand before you publish, Packaging.

Something wrong with this page?

Pages
Actions
↑↓to navigateto open