Skip to main content

Secrets

Secrets hold the sensitive values your extension needs to call external services - API keys, tokens, passwords. They are your credentials as the extension author: one set per extension, shared by every install. Streamers who install the extension never see or set them.

Where secrets can be read​

Handler typectx.secrets.get(key)
Bot-module handlers (command, keyword, pattern, event, timer, moderate)Returns the stored value, or null
Handler server functions (query, mutation, action)Returns the stored value, or null (they run on the same worker)
Automation nodesNot wired yet - always returns null
Return values reach overlay and widget tokens

Any handler server function without editorOnly: true can be called by the install's overlay, widget and extension tokens - the credentials an OBS browser source carries. Whatever such a handler returns, those callers receive. Never return a secret, or a value derived from one (a signed URL, an access token), from a handler that is not editorOnly.

Adding a secret​

Open Dashboard → Extensions → {your extension} → Secrets, click Add secret, enter the key (for example WEATHER_API_KEY) and the value, and save. The same operations are available on the API:

  • REST: GET/PUT/DELETE /v1/developer/extensions/{id}/secrets[/{key}]
  • GraphQL: developerExtensionSecrets, setExtensionSecret, deleteExtensionSecret

A developer API key (lm_dev_) may manage the secrets of the extensions it owns, so CI can set them. The CLI covers the same operations: lumio env set <KEY>, lumio env list and lumio env delete <KEY> (see CLI reference).

Rules

  • Keys match ^[A-Z0-9_]{1,64}$.
  • Values are 1 to 4096 bytes.
  • For a team extension you need the team right team-secrets:read to list keys and team-secrets:edit to set or delete one.
  • Values are never shown again after saving; listing returns key names only.

Using a secret in a bot module​

import { command } from "@zaflun/lumio-sdk/server";

export const weather = command("weather", async (ctx, args) => {
const apiKey = await ctx.secrets.get("WEATHER_API_KEY");
if (apiKey === null) {
return { reply: "Weather is not configured yet.", actions: [] };
}

const res = await ctx.fetch(`https://api.example.com/weather?q=${encodeURIComponent(args[0] ?? "Berlin")}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const data = await res.json();
return { reply: `${data.city}: ${data.temp}°C`, actions: [] };
});

ctx.secrets.get(key)​

Parameterkey: string - the key as set in the dashboard or API
ReturnsPromise<string | null> - the value, or null if no secret has that key
Throwsinvalid secret key for a key outside ^[A-Z0-9_]{1,64}$; ctx.secrets is unavailable when Lumio cannot read secrets right now
Limit10 calls per handler run

Always handle null: reply with a helpful message instead of failing silently.

Changes and rotation​

A changed or deleted secret reaches running handlers within 60 seconds. No redeploy is needed. To rotate a credential, set the new value, wait a minute, then revoke the old credential at the provider.

Security model​

PropertyValue
Encryption at restAES-256-GCM
ScopeOne set per extension, read by every install; the installing account plays no part
Who can manageThe extension's developer profile or team (team rights team-secrets:read / team-secrets:edit), including its developer API keys
Who can read valuesOnly your bot-module handlers, through ctx.secrets.get. Lumio's API never returns a value to any client
AuditSetting and deleting a secret is recorded as developer:extension_secret_set / developer:extension_secret_deleted (key name only, never the value). Reads are not audited
In transit to handlersRe-encrypted with a separate key for the bot-module worker and held there for at most 60 seconds; the plain value exists only for the duration of one ctx.secrets.get call

Never expose secrets​

Never put a secret into a chat reply, an action, a log line or ctx.cache:

// WRONG: the key ends up in chat
return { reply: `Using key ${apiKey}`, actions: [] };

// Correct: use the key only for the outbound request
const res = await ctx.fetch("https://api.example.com/data", {
headers: { Authorization: `Bearer ${apiKey}` },
});