Referencia de la API de plugins¶
El contrato con el que se escribe un plugin: los campos de plugin.yaml, los
dos servicios gRPC que intercambian el backend y el servidor, y el puente
window.evey que recibe un módulo de interfaz en su marco. La guía paso a
paso que los usa es Escribir un plugin; lo que permite cada
permiso y lo que rechaza el instalador está en
Permisos y slots.
Manifiesto¶
Cada ZIP lleva un plugin.yaml (o plugin.yml) en su raíz, de 1 MB como
máximo.
name: my-plugin # obligatorio; único, en kebab-case
version: 1.2.3 # obligatorio; semver, X.Y.Z o X.Y.Z-label
kind: plugin # obligatorio; "plugin" o "theme"
language: nodejs # obligatorio para un plugin; un runtime de la imagen
entrypoint: node index.js # obligatorio para un plugin; la línea de comandos que ejecuta el servidor
description: "..." # opcional
author: "..." # opcional
min_evey_version: "1.0.0" # opcional; se acepta y se ignora
permissions: # lo que pueden llamar el backend y la interfaz
- network
- media.read
external_fetch: # obligatorio con "network"; solo nombres de host a secas
- api.themoviedb.org
capabilities: # los papeles que el plugin dice cumplir
- metadata.provider
web_root: public/ # obligatorio si se declara frontend; se sirve como archivos estáticos
frontend:
- slot: home.section # uno de los slots permitidos
src: public/front.js # tiene que estar dentro de web_root
config_schema: # los campos del panel Config de la tarjeta del plugin
api_key:
type: password # nunca se devuelve a ningún cliente
label: API key # no se muestra: el panel rotula los campos con su clave
required: true
city:
type: string
default: Paris
description: Resolved through geocoding
| Campo | Regla |
|---|---|
name, version, kind |
Obligatorios. version tiene que seguir X.Y.Z o X.Y.Z-label; una actualización tiene que llevar una versión mayor que la instalada. |
language |
Obligatorio para kind: plugin: python, nodejs, php, ruby, lua, bash, sh o go. Elige la comprobación del runtime y los stubs que se copian junto al entrypoint; no lanza nada. |
entrypoint |
Obligatorio para kind: plugin. La primera palabra es el intérprete (o el binario, con native-binary) y el resto son sus argumentos; el proceso se ejecuta en la carpeta del plugin. |
permissions |
Cada valor tiene que estar en el catálogo de permisos. network exige un external_fetch no vacío. |
external_fetch |
Nombres de host. Un esquema y una ruta final se descartan al analizar; los hosts coinciden exactamente, un comodín *.example.com se rechaza en la instalación y un sufijo de puerto se ignora; una IP literal hace que se rechace la instalación (reglas). |
capabilities |
metadata.provider, media.read, media.write, webhook.receiver o notification.sender; todo lo demás se rechaza. Un tema no puede declarar ninguna. |
web_root |
Obligatorio si frontend no está vacío; relativo, sin .. ni barra invertida. . sirve toda la carpeta del plugin y solo anota un aviso. |
frontend[] |
slot tiene que estar en la lista de slots; src tiene que estar dentro de web_root. |
config_schema |
Un mapa de nombre de campo a {type, label, description, required, default, min, max, options}; label no se muestra, el panel rotula los campos con su clave. Tipos: string, text, password, boolean, integer, number, select (con options: [{value, label}]). |
Un tema (kind: theme) no debe declarar language, entrypoint,
permissions ni capabilities; su formato está en Temas.
Secretos. Un campo de type: password, o con secret: true, se guarda y
se entrega al backend como cualquier otro, pero vuelve enmascarado en todas
las rutas de lectura, incluido el panel Configuración de administración. Guardar
el panel con la máscara sin tocar conserva el valor guardado, así que editar
otro campo nunca borra un secreto.
Backend: los dos servicios¶
El proceso del backend y el servidor se comunican por gRPC mediante dos sockets Unix cuyas rutas llegan en el entorno:
EVEY_PLUGIN_ID=<plugin id>
EVEY_PLUGIN_SOCK=<path> # tu proceso escucha aquí y sirve PluginService
EVEY_HOST_SOCK=<path> # tu proceso se conecta aquí para llamar a HostService
El servidor autentica el proceso en su socket por el PID que informa el
núcleo (SO_PEERCRED); al proceso no se le entrega ningún token. Las
llamadas se limitan a 600 por minuto por plugin.
proto/plugin/v1/plugin_api.proto es el contrato; los stubs para los
lenguajes incluidos se copian junto al entrypoint cuando arranca el plugin
(requisitos previos).
PluginService: lo que llama el servidor¶
| Método | Cuándo |
|---|---|
Initialize(plugin_id, config, host_version) |
En cuanto el proceso ha abierto su socket. config lleva los valores de config_schema que fijó el administrador. Responde success: false con un error para negarte a arrancar. |
HealthCheck() |
De vez en cuando, para distinguir un proceso vivo de uno bloqueado. Responde healthy: true mientras puedas dar servicio. |
Shutdown(reason) |
Cuando el plugin se desactiva, se actualiza, se desinstala o el servidor se para. Termina poco después de responder. |
OnEvent(event_type, payload, source) |
Está definido en el contrato, pero hoy el servidor no entrega ningún evento a los plugins: impleméntalo para que responda handled: true, y no esperes ninguna llamada. |
GetCapabilities() |
Está definido en el contrato; hoy el servidor no lo llama. |
HostService: lo que puede llamar el plugin¶
Una llamada cuyo permiso no declara el manifiesto se rechaza con
PermissionDenied. Los métodos marcados como no servido responden
Unimplemented; forman parte del contrato para una versión futura y se
enumeran para que un stub no te confunda.
| Método | Permiso | Notas |
|---|---|---|
Log(level, message, fields) |
ninguno | Se escribe en el registro del plugin, que se ve en Registros en su tarjeta. |
GetConfig(key) / SetConfig(key, value) |
ninguno | La configuración del plugin: los valores del panel Configuración, más las claves que el plugin escribe por su cuenta. Como mucho 256 claves, claves de 256 bytes, valores de 16 KiB. |
GetData, SetData, DeleteData, ListDataKeys |
ninguno | El almacén de datos de abajo. |
EmitEvent(event_type, payload) |
ninguno | Se publica en el bus de eventos del servidor como plugin.event.<event_type>, al que se pueden suscribir los webhooks. |
GetMediaItem(id), ListMediaItems(library_id, type, parent_id, limit, offset) |
api.library o media.read |
La ficha completa del medio, con file_path, provider_ids y genres. |
Fetch(url, method, headers) |
network |
La única salida del sandbox: http o https, GET o HEAD, un host de external_fetch, las direcciones privadas rechazadas después de la resolución y a través de las redirecciones. El cuerpo se limita a 8 MiB (truncated lo indica) y la petición entera a 20 segundos. El servidor no añade ninguna credencial propia. |
SearchMedia, GetMediaStreams, GetChapters, GetSubtitles |
api.library o media.read |
No servido. |
UpdateMetadata |
api.metadata o media.write |
No servido. |
GetUserPreferences |
api.user |
No servido. |
SendNotification |
notification.sender |
No servido. |
Almacén de datos¶
Cada plugin tiene un almacén clave/valor persistente, sin ningún permiso que declarar: cursores, cachés, la hora de la última sincronización, cualquier cosa que haya que recordar entre reinicios. Los valores son cadenas opacas; codifica tú el JSON. El id del plugin sale del socket autenticado, así que un plugin solo lee y escribe en su propio espacio de nombres, y todo el almacén se borra con el plugin.
| Método | Para qué |
|---|---|
SetData(key, value) |
Crear o sustituir un valor. |
GetData(key) |
Devuelve value y found. |
DeleteData(key) |
Quitar una clave. |
ListDataKeys() |
Las claves del plugin. |
Este almacén es global para el plugin. Los datos por usuario (una
preferencia de la persona que mira el widget) se guardan del lado de la interfaz,
con el permiso prefs que se explica más abajo.
API de la interfaz¶
Un módulo de interfaz se ejecuta en un marco con un origen opaco, bajo una
política default-src 'none': sin acceso a la página anfitriona, a sus
cookies ni a su almacenamiento, sin formularios, sin ventanas emergentes, y
con un connect-src limitado a los hosts de external_fetch por https.
Todo lo demás pasa por window.evey, inyectado antes de que el módulo se
ejecute. El mismo módulo funciona sin cambios en la aplicación web y en las
aplicaciones móvil y de televisión.
evey.onInit(callback)¶
Se llama una vez con el contexto que envía la página anfitriona; un callback registrado después de que llegue el contexto se llama en el acto.
evey.onInit(function (data) {
// data.config - los valores de configuración del plugin (config_schema)
// data.context - el contexto del host
// data.locale - el idioma activo, p. ej. "en-US"
// data.theme - "dark" o "light"
// data.surface - "web", "mobile" o "tv"
// data.suggestedHeight - solo home.section: la altura (px) que recomienda la página anfitriona
// data.viewportHeight - solo tv: la altura del panel en dp
});
evey.navigate(path)¶
Lleva la aplicación anfitriona a path. Solo se siguen los prefijos
/home, /library, /media, /player, /settings y /search; todo lo
demás (la zona de administración, el inicio de sesión, una URL absoluta) se
descarta sin avisar.
evey.toast(level, message)¶
Muestra un aviso emergente en la interfaz anfitriona. level es
"success", "error" o "info"; message es texto plano, cortado a 200
caracteres; se muestran como mucho 3 avisos por segundo.
evey.resize(height)¶
Pide que el slot mida height píxeles de alto, limitado entre 50 y 800. 0
pliega el slot: un widget sin nada que mostrar no ocupa sitio. Un widget
home.section se abre a data.suggestedHeight sin hacer nada: la página
anfitriona conoce la superficie en la que ha colocado tu widget y recomienda la altura
que tiene ahí un widget del inicio, así que todos los widgets salen del mismo
tamaño. Cambia el tamaño solo si tienes un motivo concreto.
evey.prefs.* (permiso prefs)¶
Las preferencias del usuario actual en el espacio de nombres propio del plugin, ligadas a (este plugin, este usuario) por el token que la página anfitriona guarda para el marco. Cada llamada devuelve una promesa; si falta el permiso, se rechaza.
await evey.prefs.set("units", "celsius");
const { value, found } = await evey.prefs.get("units");
const all = await evey.prefs.list(); // [{ key, value }]
await evey.prefs.delete("units");
evey.media.* (permiso public)¶
Una versión segura de los elementos multimedia: id, título, tipo, año, resumen, URL del póster. Nunca una ruta de archivo, un tamaño, un id de proveedor ni un flujo.
const item = await evey.media.get(id);
const page = await evey.media.list({ libraryId });
const data = await evey.media.poster(id); // el póster como URL data:
poster devuelve una URL data: porque la política del marco prohíbe cargar
una imagen directamente desde el servidor; úsala como fuente de un <img> o
como textura.
evey.assets.read(path)¶
Lee uno de los archivos de web_root del propio plugin, por ruta relativa,
como URL data:: un archivo que el backend escribió mientras funcionaba se
puede usar en el marco sin problemas de origen cruzado. Solo se puede llegar
a los archivos del propio plugin.
En un televisor¶
La aplicación de televisión ejecuta el módulo en un WebView, y algunas cosas que son ciertas en un navegador dejan de serlo ahí:
- No calcules el tamaño a partir de tu propia ventana. Dentro del
WebView,
window.innerHeightes el slot que la página anfitriona acaba de darte. Calcula el tamaño a partir dedata.viewportHeightodata.suggestedHeight. - Ni foco ni puntero. El widget nunca recibe el foco de la cruceta y no hay efecto al pasar por encima; todo lo interactivo corresponde a las superficies web y móvil.
- Aleja el texto de los bordes. Los televisores recortan los bordes de la imagen (overscan): deja libre alrededor de un 5 % de la anchura y de la altura por cada lado.
- Prueba WebGL en un aparato real. Un shader se ejecuta en la GPU y el
controlador del aparato, no en los de tu ordenador; mantén acotadas las
coordenadas del ruido.
tests/plugins/weather-v2es el widget de referencia para todo lo anterior.
Slots¶
| Slot | Dónde aparece |
|---|---|
home.section |
Una sección de contenido en la página de inicio |
home.hero |
La zona del banner principal de la página de inicio |
library.sidebar |
Un panel lateral en las vistas de biblioteca |
media.detail.sidebar |
Un panel lateral en la página de detalle de un medio |
player.overlay |
Una capa sobre el reproductor de vídeo |
settings.user.appearance |
La sección de apariencia de los ajustes del usuario |
sidebar.bottom |
La parte de abajo de la barra lateral de navegación principal |
admin.nav |
Una entrada en el menú de administración que abre la página propia del plugin |
admin.nav es el único slot de la zona de administración. No inyecta nada en
las pantallas de administración existentes: añade una entrada de menú que
abre la página del plugin en el mismo marco con sandbox, en una ruta
reservada a los administradores. Pedirlo aparece en la diferencia que se
muestra para dar el consentimiento. Cualquier otro slot admin.*, login.*,
setup.* y profile.security.* se rechaza al instalar.
Capacidades¶
Una capacidad declara el papel que el plugin dice cumplir:
metadata.provider, media.read, media.write, webhook.receiver o
notification.sender. El instalador comprueba el nombre, el administrador ve
la lista en la vista previa y cualquier añadido al actualizar, y el Panel de
seguridad la registra. Hoy el servidor no encamina nada según una capacidad:
un metadata.provider no recibe ninguna llamada de enriquecimiento, un
webhook.receiver no recibe ninguna petición entrante. Lo que un plugin
puede hacer de verdad lo deciden sus permisos.
Permisos de la interfaz¶
public y prefs se declaran en permissions como los demás. No conceden
nada al proceso del backend; desbloquean evey.media.* y evey.prefs.* en
el marco. El marco nunca tiene un token: la página anfitriona emite un token
de cinco minutos limitado a (plugin, usuario) y hace de intermediaria en las
llamadas, y el servidor rechaza por defecto cualquier método que no nombren
los alcances del token
(ADR-0012).
Lo que un plugin nunca puede pedir
admin.*, auth.*, system.*, user.write y library.write están
bloqueados sin excepción: un manifiesto que los nombra se rechaza, un
token nunca puede llevarlos y los métodos de administración rechazan un
token de plugin, tenga los alcances que tenga.
Ejemplos¶
examples/plugins/ contiene plugins que funcionan con el formato actual del
manifiesto: plantillas de inicio en Node.js y Python (nodejs-example,
python-example), un proveedor de metadatos (metadata-example), un
receptor de webhooks (webhook-receiver), un widget de inicio con un paso de
compilación (weather), el almacén de datos con los permisos public y
prefs y el slot admin.nav (data-demo), un recorrido por todos los tipos
de campo de config_schema, con una página admin.nav (config-demo), y dos temas (theme-dark-blue,
open-workshop). make plugin-zips los empaqueta todos.