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 type | ctx.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 nodes | Not wired yet - always returns null |
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:readto list keys andteam-secrets:editto 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)
| Parameter | key: string - the key as set in the dashboard or API |
| Returns | Promise<string | null> - the value, or null if no secret has that key |
| Throws | invalid secret key for a key outside ^[A-Z0-9_]{1,64}$; ctx.secrets is unavailable when Lumio cannot read secrets right now |
| Limit | 10 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
| Property | Value |
|---|---|
| Encryption at rest | AES-256-GCM |
| Scope | One set per extension, read by every install; the installing account plays no part |
| Who can manage | The extension's developer profile or team (team rights team-secrets:read / team-secrets:edit), including its developer API keys |
| Who can read values | Only your bot-module handlers, through ctx.secrets.get. Lumio's API never returns a value to any client |
| Audit | Setting 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 handlers | Re-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}` },
});