Skip to main content

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:

LimitSync path (commands, moderation)Async path (keywords, patterns, events, timers)
CPU timeout500ms10s
Memory256 MB heap256 MB heap
Response size4 MB4 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):

  1. URL hostname check - IPv4 and IPv6 literals in the URL (https://10.0.0.1/, https://[::1]/) are rejected before any DNS lookup
  2. 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:

PermissionReview levelNotes
chat:readStandardReading chat messages
chat:sendStandardSending chat replies
events:readStandardReceiving platform events
chat:deleteEnhanced manual reviewDeleting chat messages
chat:banEnhanced manual reviewBanning 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​

CategoryLimitScope
Command execution60/minPer install
Keyword/pattern execution100/minPer install
Event handler execution50/minPer install
Timer execution1/intervalPer install
Moderationevery message, 16 checks in flight per accountPer account
Cache operations200/minPer install

Action output limits​

LimitValueScope
Actions per handler result20Per result
Actions of all kinds (chat:*, obs:*, spotify:*, …)120/minPer install

Rate limits are enforced at two layers:

  1. Input: Limits how often a handler is invoked
  2. 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 typeMaximum per extension
Commands20
Keywords50
Patterns (regex)10
Keyword minimum length3 characters
Pattern maximum length200 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 through useLumioAction record extension)
  • 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:

  1. 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
  2. The API refuses every action a bot module of that account still returns
  3. No new handler runs start; ctx.defer() jobs of a run that was already in progress finish within their 10-second budget
  4. 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:

ScenarioBehavior
Handler throws an exceptionError 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 dataTreated as null response, warning logged
ctx.db errorPromise rejects inside handler
ctx.fetch blockedPromise rejects with EgressDenied error
Rate limit exceededHandler not invoked, message silently dropped