mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 10:27:08 +02:00
* fix(plugins): align the Python HTTP example with the repo's host-call pattern
Bind http_send with raw memory offsets like nowplaying-py does, drop
guards for fields the host always sends, and document how plugins
without a PDK call host services and which built-in HTTP APIs are
disabled.
* docs(plugins): document the private-address rules for HTTP requiredHosts
Explain in the README and manifest schema that named hosts can't reach
private addresses while IP/CIDR entries and a bare "*" can.
* docs(plugins): document the private-address rules for requiredHosts
Explain in the README and manifest schema that named hosts can't reach
private addresses while IP/CIDR entries and a bare "*" can, for both
HTTP and WebSocket. Inline the single-use HTTP isHostAllowed wrapper.
* feat(plugins): derive Default for Rust host service structs
The ndpgen client.rs template now adds Default to the derive list of host
service structs, as the capability and shared types templates already do.
Plugin authors can now set only the fields they need, for example
HTTPRequest { method, url, ..Default::default() }. The webhook-rs and
discord-rich-presence-rs examples use this form now. The golden files and
the generated nd-pdk-host crate are updated to match.
* feat(plugins): deprecate pdk.NewHTTPRequest in the Go PDK
Navidrome no longer enables extism's http_request host function, so a
request built with pdk.NewHTTPRequest always fails. ndpgen now reads a small
deprecation table and writes a Deprecated: paragraph for the listed extism
functions, in both the WASM wrapper and the native stub. Linters and IDEs
now point plugin authors to host.HTTPSend. The PDK example tests used to
teach NewHTTPRequest. They now use host.HTTPSend and host.HTTPMock.
* docs(plugins): correct requiredHosts rules for websocket and private addresses
Two statements in the plugin docs did not match the code.
The WebSocket section claimed requiredHosts behaves like HTTP. It does not:
host_httpclient.go only consults the allowlist when the list is non-empty and
otherwise falls back to allowing public addresses, while host_websocket.go
always calls isHostInAllowlist, so an absent list blocks every connection.
The HTTP section claimed a named host can never reach a private address.
checkPrivateDial scans the whole requiredHosts list, so a named host does
reach a private address when the same list also holds a covering IP or CIDR.
Reworded both, plus the matching requiredHosts descriptions in
manifest-schema.json, and regenerated manifest_gen.go.
272 lines
11 KiB
Go
272 lines
11 KiB
Go
// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT.
|
|
|
|
package plugins
|
|
|
|
import "encoding/json"
|
|
import "fmt"
|
|
|
|
// Artwork service permissions for generating artwork URLs
|
|
type ArtworkPermission struct {
|
|
// Explanation for why artwork access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// Cache service permissions for storing and retrieving data
|
|
type CachePermission struct {
|
|
// Explanation for why cache access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// Configuration schema for the plugin using JSON Schema (draft-07) and optional
|
|
// JSONForms UI Schema
|
|
type ConfigDefinition struct {
|
|
// JSON Schema (draft-07) defining the plugin's configuration options
|
|
Schema map[string]interface{} `json:"schema" yaml:"schema" mapstructure:"schema"`
|
|
|
|
// Optional JSONForms UI Schema for customizing form layout
|
|
UiSchema map[string]interface{} `json:"uiSchema,omitempty" yaml:"uiSchema,omitempty" mapstructure:"uiSchema,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
func (j *ConfigDefinition) UnmarshalJSON(value []byte) error {
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal(value, &raw); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := raw["schema"]; raw != nil && !ok {
|
|
return fmt.Errorf("field schema in ConfigDefinition: required")
|
|
}
|
|
type Plain ConfigDefinition
|
|
var plain Plain
|
|
if err := json.Unmarshal(value, &plain); err != nil {
|
|
return err
|
|
}
|
|
*j = ConfigDefinition(plain)
|
|
return nil
|
|
}
|
|
|
|
// HTTP access permissions for a plugin
|
|
type HTTPPermission struct {
|
|
// Explanation for why HTTP access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
|
|
// List of required host patterns for HTTP requests (e.g., 'api.example.com',
|
|
// '*.musicbrainz.org'). A named host alone can't reach a private address; also
|
|
// list an IP, a CIDR (e.g., '10.0.0.0/8') or '*' for that
|
|
RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"`
|
|
}
|
|
|
|
// Key-value store permissions for persistent plugin storage
|
|
type KVStorePermission struct {
|
|
// Maximum storage size (e.g., '1MB', '500KB'). Default: 1MB
|
|
MaxSize *string `json:"maxSize,omitempty" yaml:"maxSize,omitempty" mapstructure:"maxSize,omitempty"`
|
|
|
|
// Explanation for why key-value store access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// Library service permissions for accessing library metadata and optionally
|
|
// filesystem
|
|
type LibraryPermission struct {
|
|
// Whether the plugin requires read-only filesystem access to library directories
|
|
Filesystem bool `json:"filesystem,omitempty" yaml:"filesystem,omitempty" mapstructure:"filesystem,omitempty"`
|
|
|
|
// Explanation for why library access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
func (j *LibraryPermission) UnmarshalJSON(value []byte) error {
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal(value, &raw); err != nil {
|
|
return err
|
|
}
|
|
type Plain LibraryPermission
|
|
var plain Plain
|
|
if err := json.Unmarshal(value, &plain); err != nil {
|
|
return err
|
|
}
|
|
if v, ok := raw["filesystem"]; !ok || v == nil {
|
|
plain.Filesystem = false
|
|
}
|
|
*j = LibraryPermission(plain)
|
|
return nil
|
|
}
|
|
|
|
// Plugin manifest for Navidrome plugins
|
|
type Manifest struct {
|
|
// The author of the plugin
|
|
Author string `json:"author" yaml:"author" mapstructure:"author"`
|
|
|
|
// Config corresponds to the JSON schema field "config".
|
|
Config *ConfigDefinition `json:"config,omitempty" yaml:"config,omitempty" mapstructure:"config,omitempty"`
|
|
|
|
// A brief description of what the plugin does
|
|
Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"`
|
|
|
|
// The display name of the plugin
|
|
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
|
|
|
// Permissions corresponds to the JSON schema field "permissions".
|
|
Permissions *Permissions `json:"permissions,omitempty" yaml:"permissions,omitempty" mapstructure:"permissions,omitempty"`
|
|
|
|
// The version of the plugin (semver recommended)
|
|
Version string `json:"version" yaml:"version" mapstructure:"version"`
|
|
|
|
// URL to the plugin's website or repository
|
|
Website *string `json:"website,omitempty" yaml:"website,omitempty" mapstructure:"website,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
func (j *Manifest) UnmarshalJSON(value []byte) error {
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal(value, &raw); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := raw["author"]; raw != nil && !ok {
|
|
return fmt.Errorf("field author in Manifest: required")
|
|
}
|
|
if _, ok := raw["name"]; raw != nil && !ok {
|
|
return fmt.Errorf("field name in Manifest: required")
|
|
}
|
|
if _, ok := raw["version"]; raw != nil && !ok {
|
|
return fmt.Errorf("field version in Manifest: required")
|
|
}
|
|
type Plain Manifest
|
|
var plain Plain
|
|
if err := json.Unmarshal(value, &plain); err != nil {
|
|
return err
|
|
}
|
|
if len(plain.Author) < 1 {
|
|
return fmt.Errorf("field %s length: must be >= %d", "author", 1)
|
|
}
|
|
if len(plain.Name) < 1 {
|
|
return fmt.Errorf("field %s length: must be >= %d", "name", 1)
|
|
}
|
|
if len(plain.Version) < 1 {
|
|
return fmt.Errorf("field %s length: must be >= %d", "version", 1)
|
|
}
|
|
*j = Manifest(plain)
|
|
return nil
|
|
}
|
|
|
|
// Matcher service permissions for resolving external songs to local library tracks
|
|
type MatcherPermission struct {
|
|
// Explanation for why matcher access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// Permissions required by the plugin
|
|
type Permissions struct {
|
|
// Artwork corresponds to the JSON schema field "artwork".
|
|
Artwork *ArtworkPermission `json:"artwork,omitempty" yaml:"artwork,omitempty" mapstructure:"artwork,omitempty"`
|
|
|
|
// Cache corresponds to the JSON schema field "cache".
|
|
Cache *CachePermission `json:"cache,omitempty" yaml:"cache,omitempty" mapstructure:"cache,omitempty"`
|
|
|
|
// Http corresponds to the JSON schema field "http".
|
|
Http *HTTPPermission `json:"http,omitempty" yaml:"http,omitempty" mapstructure:"http,omitempty"`
|
|
|
|
// Kvstore corresponds to the JSON schema field "kvstore".
|
|
Kvstore *KVStorePermission `json:"kvstore,omitempty" yaml:"kvstore,omitempty" mapstructure:"kvstore,omitempty"`
|
|
|
|
// Library corresponds to the JSON schema field "library".
|
|
Library *LibraryPermission `json:"library,omitempty" yaml:"library,omitempty" mapstructure:"library,omitempty"`
|
|
|
|
// Matcher corresponds to the JSON schema field "matcher".
|
|
Matcher *MatcherPermission `json:"matcher,omitempty" yaml:"matcher,omitempty" mapstructure:"matcher,omitempty"`
|
|
|
|
// Scheduler corresponds to the JSON schema field "scheduler".
|
|
Scheduler *SchedulerPermission `json:"scheduler,omitempty" yaml:"scheduler,omitempty" mapstructure:"scheduler,omitempty"`
|
|
|
|
// ScrobbleRetriever corresponds to the JSON schema field "scrobbleRetriever".
|
|
ScrobbleRetriever *ScrobbleRetrieverPermission `json:"scrobbleRetriever,omitempty" yaml:"scrobbleRetriever,omitempty" mapstructure:"scrobbleRetriever,omitempty"`
|
|
|
|
// Storage corresponds to the JSON schema field "storage".
|
|
Storage *StoragePermission `json:"storage,omitempty" yaml:"storage,omitempty" mapstructure:"storage,omitempty"`
|
|
|
|
// Subsonicapi corresponds to the JSON schema field "subsonicapi".
|
|
Subsonicapi *SubsonicAPIPermission `json:"subsonicapi,omitempty" yaml:"subsonicapi,omitempty" mapstructure:"subsonicapi,omitempty"`
|
|
|
|
// Taskqueue corresponds to the JSON schema field "taskqueue".
|
|
Taskqueue *TaskQueuePermission `json:"taskqueue,omitempty" yaml:"taskqueue,omitempty" mapstructure:"taskqueue,omitempty"`
|
|
|
|
// Users corresponds to the JSON schema field "users".
|
|
Users *UsersPermission `json:"users,omitempty" yaml:"users,omitempty" mapstructure:"users,omitempty"`
|
|
|
|
// Websocket corresponds to the JSON schema field "websocket".
|
|
Websocket *WebSocketPermission `json:"websocket,omitempty" yaml:"websocket,omitempty" mapstructure:"websocket,omitempty"`
|
|
}
|
|
|
|
// Scheduler service permissions for scheduling tasks
|
|
type SchedulerPermission struct {
|
|
// Explanation for why scheduler access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// Scrobble retriever permissions for retrieving scrobbles from users
|
|
type ScrobbleRetrieverPermission struct {
|
|
// Explanation for why scrobble retriever access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// Storage permissions for enabling persistent read-write storage exclusively for
|
|
// the plugin
|
|
type StoragePermission struct {
|
|
// Explanation for why storage access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// SubsonicAPI service permissions. Requires 'users' permission to be declared.
|
|
type SubsonicAPIPermission struct {
|
|
// Explanation for why SubsonicAPI access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// Task queue permissions for background task processing
|
|
type TaskQueuePermission struct {
|
|
// Maximum total concurrent workers across all queues. Default: 1
|
|
MaxConcurrency int `json:"maxConcurrency,omitempty" yaml:"maxConcurrency,omitempty" mapstructure:"maxConcurrency,omitempty"`
|
|
|
|
// Explanation for why task queue access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
func (j *TaskQueuePermission) UnmarshalJSON(value []byte) error {
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal(value, &raw); err != nil {
|
|
return err
|
|
}
|
|
type Plain TaskQueuePermission
|
|
var plain Plain
|
|
if err := json.Unmarshal(value, &plain); err != nil {
|
|
return err
|
|
}
|
|
if v, ok := raw["maxConcurrency"]; !ok || v == nil {
|
|
plain.MaxConcurrency = 1.0
|
|
}
|
|
if 1 > plain.MaxConcurrency {
|
|
return fmt.Errorf("field %s: must be >= %v", "maxConcurrency", 1)
|
|
}
|
|
*j = TaskQueuePermission(plain)
|
|
return nil
|
|
}
|
|
|
|
// Users service permissions for accessing user information
|
|
type UsersPermission struct {
|
|
// Explanation for why users access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
}
|
|
|
|
// WebSocket service permissions for establishing WebSocket connections
|
|
type WebSocketPermission struct {
|
|
// Explanation for why WebSocket access is needed
|
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
|
|
|
// List of required host patterns for WebSocket connections (e.g.,
|
|
// 'api.example.com', '*.musicbrainz.org'). Required: with no entries every
|
|
// connection is blocked. A named host alone can't reach a private address; also
|
|
// list an IP, a CIDR (e.g., '10.0.0.0/8') or '*' for that
|
|
RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"`
|
|
}
|