The settings form (apiVersion 6)¶
The usual settings hold values your code reads with kino.config. From
"apiVersion": 6 (Kino 0.9.50) the form can also show and do things, and every installed plugin
gets its own tab in Ajustes. This page has every field type, the three types without
a value, the exports that fill and check them, and a complete example.
Its own tab in Ajustes¶
Every installed plugin that is enabled gets its own tab in Ajustes (phone and TV), named after the
plugin, with its Modo debug switch; when it has settings the tab holds their
form too, and Plugins ▸ Configurar opens the same form. That is why, from Kino 0.9.50,
the auth_required error reads "Configura {plugin} en Ajustes ▸ {plugin}" when your plugin declares
settings.
Every field type¶
The form shows your settings in the order of the manifest, each with its label and, under it, its
hint. Nine types:
type |
What the person sees | What kino.config.get(key) returns |
Since |
|---|---|---|---|
text |
a text field (at most 500 characters) | the text, or undefined |
apiVersion 1 |
password |
a hidden text field (at most 500), kept encrypted on the device | the text, or undefined |
apiVersion 1 |
url |
an address field (at most 2,048): http/https; that server becomes a host your plugin may reach (The person's own servers) |
the address as typed, or undefined |
apiVersion 1 |
toggle |
a switch | true / false (false when there is no default) |
apiVersion 1 |
select |
a choice among options |
the chosen value (the first option when there is no default) |
apiVersion 1 |
list |
a list the person builds with "Agregar", each entry a dialog of the list's fields |
an array of { [field key]: string }, or undefined when empty |
apiVersion 4 |
section |
a heading, with hint as its explanation |
nothing | apiVersion 6 |
status |
a read-only line your settingsStatus() fills |
nothing | apiVersion 6 |
action |
a button that runs your action(key) |
nothing | apiVersion 6 |
What each entry may carry:
| Attribute | Types | Rule |
|---|---|---|
key |
all | required, ^[a-z][a-zA-Z0-9_]{0,31}$, unique in the list |
label |
all | required, 1 to 40 characters |
type |
all | required, one of the nine above |
hint |
all | optional, at most 80 characters: the example or explanation under the field. On a section, up to 300, wrapped over several lines (Kino 0.9.51; builds before 0.9.51 refuse one over 80) |
required |
text, password, url, list |
optional true/false. A required setting with no value stops every call ("Falta configurar") |
default |
text, password, toggle, select |
optional. Never on url or list ("… no puede tener valor por defecto: usa "hint"") nor on the three types without a value; it must fit its type (a select default is one of its values) |
options |
select only |
required: 1 to 20 { "value", "label" }, value 1..40 characters and unique, label 1..40 |
fields |
list only |
required: 1 to 4, each { key, label, type, hint?, required? } with type "text" or "url", no default ("Solo un ajuste de tipo list tiene "fields"") |
max |
list only |
optional whole number 1..50 (default 20): how many entries |
confirm |
action only, apiVersion 6 |
optional, 1 to 120 characters: asked before the action runs, with Cancelar focused ("Solo un ajuste de tipo action tiene "confirm"") |
Any other key in an entry is ignored. A manifest that breaks a rule is refused at install with a
message that names the setting (El ajuste "quality" necesita opciones); a list below apiVersion 4
or a section/status/action below apiVersion 6 is refused too. Limits: 12 settings with a value
(text, password, url, toggle, select, list) plus, from apiVersion 6, 16 without one.
There are no conditional fields: every setting is always shown, whatever another one holds. Use a
section with a hint to say which fields go together ("Opcional: sin cuenta ves el catálogo gratis"),
make them optional, and let validateSettings refuse a combination that makes no sense.
password or sealed secrets? A password setting is the person's credential: they type it,
your code reads it, it syncs to their other devices sealed end to end. A key that belongs to you, the
author (a fixed API key of the site's own player), goes in the manifest's sealed
secrets instead, and your code only ever holds a marker.
Three types that hold no value¶
From apiVersion 6 there are three types that hold no value (never in kino.config, never required,
no default), at most 16 of them on top of the 12 valued settings:
"settings": [
{ "key": "account", "label": "Tu cuenta", "type": "section", "hint": "Opcional: sin cuenta usas la sesión anónima" },
{ "key": "email", "label": "Correo", "type": "text" },
{ "key": "password", "label": "Contraseña", "type": "password" },
{ "key": "linked", "label": "Estado", "type": "status" },
{ "key": "logout", "label": "Cerrar sesión", "type": "action", "confirm": "¿Cerrar la sesión de esta cuenta?" }
]
section: a heading;hintis its explanation.status: a read-only line. Kino calls yoursettingsStatus()when the form opens (10 s) and shows the text you return under that key:{ linked: "Vinculada como ana@…" }(at most 200 characters). Until it answers the line reads "Cargando…"; a missing key or a value that is not a text shows "Sin información"; a timeout or an error shows "No se pudo consultar". The rest of the form keeps working either way. Required export when astatussetting exists.action: a button. Kino calls youraction(key)(30 s), one action at a time (the other buttons and Guardar wait), and shows themessageyou return (at most 300 characters) or "Listo"; if it throws or times out, the person sees the error text instead. Kino askssettingsStatus()again after every action (and when the form opens), so the status lines describe what it just did;refresh: trueis still accepted and changes nothing.confirm(1 to 120 characters) asks first, with Cancelar focused. Required export when anactionsetting exists.
Forgetting settings from an action (clearSettings)¶
An action may also forget settings of your own with clearSettings: up to 12 keys of your declared
valued settings (text, url, password, toggle, select, list). After the action returns, Kino
empties them exactly as if the person had emptied the fields and pressed Guardar (a password also
leaves the Keystore), closes your sandbox and forgets cookies and cached Home rows as for any saved
change (kino.storage survives), reloads the form and asks settingsStatus() again; your message is
still shown. It is how a "Cerrar sesión" button stops the next expired token from signing the person in
again with the saved account.
export async function action(key) {
if (key === "logout") {
await api.logout(); // tell the server first: a throw here keeps the saved account
return { message: "Sesión cerrada", clearSettings: ["email", "password"] };
}
}
(email and password must be optional settings here; mark the account required only if the plugin
cannot work signed out.)
Limits:
- Only the action's own plugin is ever touched.
- An entry that is not a string, is unknown, names a
section/status/action, names arequiredsetting (clearing it would make every later call fail), repeats, or comes after the twelfth is dropped with a log line (run.mjsshows[dropped by Kino]); a value that is not an array is ignored. - An action that throws or times out clears nothing.
Checking before saving (validateSettings)¶
validateSettings(values) (optional, 20 s) runs before Kino saves. values holds only the valued
settings: strings trimmed, toggles as booleans, lists as arrays of objects.
- Return
nullto accept. { password: "La contraseña no es correcta" }refuses with that text under the field (at most 200 characters), or a text refuses with a general message. A key that is not one of your valued settings also refuses, its text shown as the general message.- Nothing is saved on a refusal and the person keeps what they typed.
- If it throws, times out or answers something Kino cannot read (an array, a number…), nothing is saved either, and the form offers "Guardar sin comprobar"; that button is offered only then, never after a refusal with messages.
Common rules¶
- Texts you return are shown after Kino removes any of your stored secrets from them.
- These three exports run even while a
requiredsetting is still empty, so a status line can say what is missing. - Saving any setting closes your sandbox, as always;
kino.storagesurvives. - If you declare
telemetry, a failure ofsettingsStatus,actionorvalidateSettingsis reported like any other function's (Logs and telemetry).
Try them with the kit (it prints what the app would keep): node sdk/run.mjs . settingsStatus,
node sdk/run.mjs . action logout, node sdk/run.mjs . validateSettings '{"email":"ana@x.co"}' (--raw
prints your answer untouched). The kit does not apply the app's secret scrubbing, and
validateSettings receives exactly the JSON you type: the app sends only valued settings, trimmed. A
working example is plugins/sdk/test/settings-demo in Kino's repository.
A complete example¶
An account that is optional, a status line, a "Cerrar sesión" button, a quality choice, a switch and a
list of the person's own mirrors, all read the way kino.d.ts declares them:
{
"id": "mi-cuenta", "name": "Mi cuenta", "version": "1.0.0", "apiVersion": 6, "entry": "plugin.js",
"hosts": ["api.example.com"],
"capabilities": ["search", "resolve"],
"settings": [
{ "key": "account", "label": "Tu cuenta", "type": "section", "hint": "Opcional: sin cuenta ves el catálogo gratis" },
{ "key": "email", "label": "Correo", "type": "text", "hint": "ana@correo.com" },
{ "key": "password", "label": "Contraseña", "type": "password" },
{ "key": "linked", "label": "Estado", "type": "status" },
{ "key": "logout", "label": "Cerrar sesión", "type": "action", "confirm": "¿Cerrar la sesión de esta cuenta?" },
{ "key": "playback", "label": "Reproducción", "type": "section" },
{ "key": "quality", "label": "Calidad", "type": "select", "default": "auto",
"options": [{ "value": "auto", "label": "Automática" }, { "value": "hd", "label": "Alta" }, { "value": "sd", "label": "Ahorro de datos" }] },
{ "key": "spanishSubs", "label": "Solo subtítulos en español", "type": "toggle", "default": true },
{ "key": "mirrors", "label": "Espejos propios", "type": "list", "max": 5,
"fields": [{ "key": "url", "label": "Dirección", "type": "url", "required": true, "hint": "https://espejo.example.org" },
{ "key": "name", "label": "Nombre", "type": "text", "hint": "Espejo de la casa" }] }
]
}
const API = "https://api.example.com";
// Keyed by the account: settings can change under the plugin (another device, clearSettings).
const sessionKey = () => `session:${kino.config.get("email") ?? ""}`;
async function login(email, password) {
const r = await kino.fetch(`${API}/login`, { method: "POST", body: { json: { email, password } } });
if (r.status === 401) throw kino.error("auth_required", "login 401");
if (!r.ok) throw kino.error("unavailable", `login ${r.status}`);
return r.json().token;
}
async function session() {
await null; // the first await before anything can throw
const email = kino.config.get("email"); // undefined: signed out, free catalog
if (!email) return null;
const saved = kino.storage.get(sessionKey());
if (saved) return saved;
const token = await login(email, kino.config.get("password") ?? "");
kino.storage.set(sessionKey(), token, { ttlMs: 12 * 60 * 60 * 1000 });
return token;
}
export async function settingsStatus() { // one text per "status" key
const email = kino.config.get("email");
if (!email) return { linked: "Sin cuenta: ves el catálogo gratis" };
try {
await session();
return { linked: "Cuenta vinculada" };
} catch (e) {
return { linked: e.code === "auth_required" ? "El correo o la contraseña no coinciden" : "No se pudo revisar la cuenta ahora" };
}
}
export async function action(key) { // one call per "action" button
if (key !== "logout") return null;
const token = kino.storage.get(sessionKey());
if (token) await kino.fetch(`${API}/logout`, { method: "POST", headers: { Authorization: `Bearer ${token}` } });
kino.storage.remove(sessionKey());
return { message: "Sesión cerrada", clearSettings: ["email", "password"] };
}
export async function validateSettings(values) { // before saving: null accepts
await null;
if (values.email && !values.password) return { password: "Escribe la contraseña de esa cuenta" };
if (values.password && !values.email) return { email: "Escribe el correo de la cuenta" };
if (values.email) {
try {
await login(values.email, values.password);
} catch (e) {
if (e.code === "auth_required") return { password: "El correo o la contraseña no coinciden" };
throw e; // the person may "Guardar sin comprobar"
}
}
return null;
}
export async function search(query) {
const token = await session();
const r = await kino.fetch(`${API}/search?q=${encodeURIComponent(query.q)}`,
{ headers: token ? { Authorization: `Bearer ${token}` } : {} });
if (!r.ok) throw kino.error("unavailable", `search ${r.status}`);
return r.json().results.map((x) => ({ id: String(x.id), ref: String(x.id), title: x.title, kind: "movie" }));
}
export async function resolve(ref) {
const token = await session();
const quality = kino.config.get("quality"); // "auto", "hd" or "sd": a select always has a value
const r = await kino.fetch(`${API}/play/${encodeURIComponent(ref)}?q=${quality}`,
{ headers: token ? { Authorization: `Bearer ${token}` } : {} });
if (!r.ok) throw kino.error("not_found", `play ${r.status}`);
const s = r.json(); // { url, path, subtitles: [{ lang, url }] }
const mirrors = kino.config.get("mirrors") ?? []; // [{ url, name? }]: each url is a server the person typed
return {
url: s.url,
label: "Principal",
subtitles: kino.config.get("spanishSubs") ? s.subtitles.filter((t) => t.lang === "es") : s.subtitles,
alternatives: mirrors.slice(0, 8).map((m) => ({ url: m.url.replace(/\/+$/, "") + s.path, label: m.name || "Espejo" })),
};
}
What the person gets: a tab "Mi cuenta" in Ajustes with two headings; "Estado" reads "Sin cuenta: ves el catálogo gratis" until they type an account; Guardar checks the account before saving it; "Cerrar sesión" asks first, then empties the correo and the contraseña on every device of theirs; each mirror shows up as one more copy in the player's Servidor menu.
On the person's other devices¶
When the person pairs two of their devices (phone and TV), Kino keeps their plugins in step. What crosses, and what does not:
- Crosses: install, updates, on/off, uninstall, and the host approvals. Each device fetches your code itself; only the address and the approval travel. The other device installs silently when what it fetched asks for nothing beyond what the person approved on the first one; if you now ask for more (a new host, permission or capability), it waits in "Plugins de tus otros aparatos" for the normal consent sheet.
- Crosses: the values of
text,select,toggle,urlandlistsettings (alistthat has apasswordfield does not travel), andpasswordsettings, sealed end to end with the key the devices agreed when they paired, never readable on the way. - Crosses: clears. A field the person empties, or an action's
clearSettings, empties it on their other devices too (passwords included). Arequiredsetting is never cleared remotely. A device running an older Kino build ignores clears and keeps its value. - Never crosses:
kino.storage, cookies, the cached Home rows.
A value that arrives this way is applied without your validateSettings, and, like any saved
change, closes your sandbox. So write the plugin as if a setting can change under it at any time: key
any session you keep in kino.storage by the account it belongs to, and never put a device identity in
a setting, because it would be copied to the other device.
const sessionKey = `session:${kino.config.get("email") ?? ""}`; // not just "session"
A signed plugin installed from two different repositories on two devices counts as the
same plugin when both have the same id and the same author key: see
The same plugin at two addresses.