Bot Module Security
Bot modules execute third-party code that can send chat messages and perform moderation actions. Lumio enforces multiple security layers to protect streamers, viewers, and the platform.
V8 isolation
Every handler invocation runs inside an isolated V8 sandbox with strict resource limits:
| Limit | Sync path (commands, moderation) | Async path (keywords, patterns, events, timers) |
|---|---|---|
| CPU timeout | 500ms | 10s |
| Memory | 256 MB heap | 256 MB heap |
| Response size | 4 MB | 4 MB |
Each extension install gets its own V8 isolate. Isolates share no state - one extension cannot access another extension's memory, variables, or closures.
The timeout is enforced inside the worker, on the thread that runs the isolate, for synchronous code and pending async work alike; on expiry the isolate is terminated and discarded. Handler code cannot reach the host runtime (Deno, Deno.core and __bootstrap are removed before it runs). See Handler runtime limits for the full list, including the per-worker concurrency cap.
Database isolation
Each extension gets its own PostgreSQL schema (ext_\{extension_id\}), shared by every account that installs it. Every table carries an _account_id column and every query is scoped to the calling account, so one account never reads another account's rows. Extensions cannot query core Lumio tables (users, accounts, overlays) or other extensions' schemas. When an account uninstalls the extension, its rows are deleted (at once or after the 30-day retention window); the schema is dropped only with the last install.
Cache isolation
ctx.cache operations are scoped to one Redis hash per installation (lumio:ext:cache:\{install_id\}); the handler only names a field of it. The Rust worker picks the hash from the install it runs, so extensions cannot access other installations' cache keys or any internal Lumio Redis keys. The hash is capped at 256 keys of 4 KB, every key expires within 24 hours, and uninstalling deletes it.
Egress restrictions
ctx.fetch() only allows HTTPS requests to hostnames declared in the egress.allowHosts manifest field. A wildcard entry must cover a registrable domain (*.example.com, never *.com); manifest validation rejects anything broader. Only globally routable unicast addresses can be reached — private, loopback, link-local, multicast, documentation and other special-purpose IPv4/IPv6 ranges, including NAT64/6to4 addresses that embed such an IPv4 address, are blocked at two layers (full list: Blocked address ranges):
- URL hostname check - IPv4 and IPv6 literals in the URL (
https://10.0.0.1/,https://[::1]/) are rejected before any DNS lookup - Post-DNS-resolution check - every resolved IP is validated against the same ranges and the connection is pinned to the validated address, preventing DNS rebinding attacks
Redis isolation
Extension JavaScript has no direct Redis access. There is no ctx.redis API. All Redis operations (ctx.cache, cooldowns, rate limits, pub/sub) are performed by trusted Rust code in the Worker. Redis keys are constructed from validated install IDs - extension input is never interpolated into key names.
Permission tiers
Extensions declare required permissions in lumio.config.json. Each permission goes through store review:
| Permission | Review level | Notes |
|---|---|---|
chat:read | Standard | Reading chat messages |
chat:send | Standard | Sending chat replies |
events:read | Standard | Receiving platform events |
chat:delete | Enhanced manual review | Deleting chat messages |
chat:ban | Enhanced manual review | Banning users (a timeout is the chat:timeout action) |
Extensions requesting chat:ban or chat:delete undergo additional manual code review during the store approval process. Reviewers verify that moderation logic is sound and that the extension cannot be exploited to mass-ban or mass-delete.
Rate limits
All actions are rate-limited per installation to prevent abuse:
Handler invocation limits
| Category | Limit | Scope |
|---|---|---|
| Command execution | 60/min | Per install |
| Keyword/pattern execution | 100/min | Per install |
| Event handler execution | 50/min | Per install |
| Timer execution | 1/interval | Per install |
| Moderation | every message, 16 checks in flight per account | Per account |
| Cache operations | 200/min | Per install |
Action output limits
| Limit | Value | Scope |
|---|---|---|
| Actions per handler result | 20 | Per result |
Actions of all kinds (chat:*, obs:*, spotify:*, …) | 120/min | Per install |
Rate limits are enforced at two layers:
- Input: Limits how often a handler is invoked
- Output: Limits how many actions an install can run, regardless of which handler type produced them - command, keyword, pattern, event, timer and moderation actions share one counter
Actions beyond the per-minute cap are refused (rate_limited) until the next minute; if Lumio cannot count them, they are refused as well. Actions run at most once - a failed action is never retried.
Trigger limits
Each extension is limited in how many triggers it can declare:
| Trigger type | Maximum per extension |
|---|---|
| Commands | 20 |
| Keywords | 50 |
| Patterns (regex) | 10 |
| Keyword minimum length | 3 characters |
| Pattern maximum length | 200 characters |
Regex patterns are validated for safety. The Rust regex crate guarantees linear-time matching (no backreferences or lookaheads), making ReDoS attacks impossible by construction. Pattern matching runs in Rust on the bot side - JavaScript RegExp is never used for trigger matching.
Moderation fail-open policy
Moderation never holds chat up. The bot calls moderate handlers beside the chat path with a 500ms budget; when a call times out, fails, or the account already has 16 checks in flight, the message passes through. Messages from the broadcaster are not moderated; moderator messages reach the handler with ctx.user.isMod set, and Discord messages are not passed to moderate handlers.
A decision runs only as the chat action it maps to (chat:delete, chat:timeout, chat:ban, chat:send), so the manifest permission gate and the per-install action cap apply to it like to any other action.
Install log and audit log
Every action outcome - success, refusal or failure - and every non-allow moderation decision or error is written to the install's log, at most one entry per second per install. Entries carry the action name and, for chat actions, the platform and target ids; message text and other params are never stored. An allow writes nothing.
Bans, unbans and timeouts a bot module issues are also written to the account's audit log as account:extension_moderation with:
- Source:
bot_module(widgets and apps acting throughuseLumioActionrecordextension) - Metadata: extension ID, install ID, action, platform, target user ID, timeout duration
- Reason: never recorded
A bot module's message deletions are recorded in the install log only, so a moderation module clearing a spam raid does not flood the audit log.
Kill switch
Every account has a "Pause all extension modules" toggle in the dashboard. When activated:
- A signal is published to all connected bots and to the Bot Module Worker, which stop dispatching commands, keywords, patterns, events, timers and moderation for that account
- The API refuses every action a bot module of that account still returns
- No new handler runs start;
ctx.defer()jobs of a run that was already in progress finish within their 10-second budget - The built-in modules (Link Protection, Word Filter, Spam Protection, Timed Messages) are system bot-module installs and stop as well
The kill switch is designed for emergencies - if a malicious extension starts banning legitimate users, the streamer can stop all extension modules with one click.
Built-in modules
Built-in modules (Link Protection, Word Filter, Spam Protection) are system extensions with defineModerate handlers and run through the same path as yours. Every moderation install of the account is called independently for each message; one module's decision does not stop another from being called.
Error isolation
Handler errors are contained within the isolate:
| Scenario | Behavior |
|---|---|
| Handler throws an exception | Error logged, no message sent to chat |
| Handler times out (sync) | Command silently dropped; moderation lets the message through |
| Handler times out (async) | Handler killed, error logged |
| Handler returns invalid data | Treated as null response, warning logged |
ctx.db error | Promise rejects inside handler |
ctx.fetch blocked | Promise rejects with EgressDenied error |
| Rate limit exceeded | Handler not invoked, message silently dropped |