A .funplugin is a UTF-8 JSON declaration consumed by a host-provided
adapter. It declares one public Discourse provider and display metadata. It is
not an extension runtime and cannot contain executable behavior.
The current contract is:
apiVersion = 1
engine = discourse-script-source-v1
The JSON Schemas are available under
packages/sdk/schema, but they are editor and
object-shape aids rather than conformance authorities. Standard JSON Schema
cannot compare normalized values across fields or inspect decoded PNG chunks.
Only validateManifest or funplugin validate makes a complete v1
conformance decision. Schema success alone must never be presented as package
validity.
An unsigned .funplugin is exactly one strict manifest object. The package is
valid only when it is valid UTF-8 JSON and contains no unknown fields.
{
"id": "example.community",
"version": "1.0.0",
"apiVersion": 1,
"engine": "discourse-script-source-v1",
"baseUrl": "https://community.example.com",
"allowedOrigins": ["https://community.example.com"],
"metadata": {
"displayName": "Example Community",
"description": "Search the example community for scripts.",
"homepage": "https://community.example.com/about",
"publisher": {
"name": "Example Publisher",
"url": "https://publisher.example.org/"
},
"permissions": [
{ "capability": "authentication", "reason": "Sign in to the community." },
{ "capability": "search", "reason": "Search public script topics." },
{ "capability": "script-download", "reason": "Download a selected script." }
],
"searchQuerySuffix": "#scripts"
}
}Unsigned serialization produced by the SDK is two-space indented JSON followed by a newline. Consumers must validate the parsed value rather than treating formatting as identity. The package SHA-256 is calculated over the emitted UTF-8 artifact, including its terminal newline.
| Field | Requirement |
|---|---|
id |
3–80 lowercase letters, digits, dots, and hyphens; valid semantic namespace form; no reserved Windows device prefix. |
version |
Semantic Versioning 2.0 string. |
apiVersion |
Integer 1. |
engine |
Exact string discourse-script-source-v1. |
baseUrl |
Public HTTPS origin on port 443, no path, credentials, query, or fragment. |
allowedOrigins |
Array containing exactly one value, exactly equal to baseUrl. |
metadata |
Strict metadata object described below. |
baseUrl must use a public DNS hostname. Literal IP addresses and local,
loopback, private, link-local, test, and internal-style hostnames are rejected.
Required fields are displayName, description, homepage, publisher, and
permissions. icon and searchQuerySuffix are optional. All strings are
trimmed and exclude control characters.
displayName: 1–80 characters.description: 1–500 characters.homepage: public HTTPS URL on the provider origin.publisher.name: 1–100 characters; verification marks are prohibited.publisher.url: optional public HTTPS URL.searchQuerySuffix: optional 1–200 character search text appended by the adapter.icon: optional non-animated PNG. Its canonical padded base64 data is at most 32 KiB decoded; dimensions are at most 512×512 and 262,144 pixels.
v1 requires this exact set, each exactly once, with a 1–240 character reason:
authentication
search
script-download
The declarations describe the fixed adapter behavior; they do not grant a package arbitrary runtime access. Optional or additional capabilities are not part of v1.
The discourse-script-source-v1 adapter uses only the declared origin and
these provider paths:
| Operation | Path |
|---|---|
| Search | /search.json?q=... |
| Topic detail | /t/{topicId}.json |
| Current session | /session/current.json |
| Sign-in page | /login |
Search and topic responses are JSON. The conformance probe accepts bounded JSON responses, follows at most three redirects that remain on the declared origin, and checks Discourse-shaped topic data. The default Node.js probe resolves the provider once, rejects any non-public result, and pins the HTTPS socket lookup to the validated addresses. A per-request deadline remains active until the bounded response body is complete. It reports diagnostics only; the host remains responsible for runtime enforcement.
Eligible download URLs are HTTPS, have exactly the declared origin, and end in
.funscript (case-insensitive). A package cannot declare URL templates,
selectors, arbitrary request headers, alternate origins, archive handling,
media handling, or an asset-opening capability.
A signed release wraps the same manifest in a canonical JSON envelope:
{
"signed": {
"type": "scriptplayerplus.script-source.release",
"formatVersion": 1,
"publisherId": "example.publisher",
"keyId": "ed25519-sha256:base64url-sha256-of-spki",
"manifest": { "...": "the v1 manifest" }
},
"signature": "base64url-ed25519-signature"
}The signed payload is canonical JSON with lexicographically ordered object
keys. The signing input is UTF-8 bytes of:
ScriptPlayerPlus.funplugin.v1\0 + canonical-json(signed payload)
The signature is Ed25519 and uses unpadded base64url. keyId is
ed25519-sha256: followed by the base64url SHA-256 digest of the public key's
SPKI DER encoding. Signed artifact JSON must itself be canonical, with only an
optional final newline.
A valid signature proves control of the corresponding private key for that artifact. It does not prove that a host release admits the publisher, the key, or the package.
v1 does not support executable modules, custom adapters, custom HTTP request templates, multi-origin packages, package-managed updates, arbitrary file downloads, archive extraction, media opening, or host trust decisions in the public SDK.