Venora v0.1.0 · in development

Developers · Plugin API v1

Build a plugin in two files.

A Venora plugin is a plugin.json manifest and one ES module. This page is the guided tour. The API v1 specification in the repository is the contract, and you can write a plugin from it without reading Venora's source.

01 Quick start

Structure

Put the files in a folder and ZIP it. plugin.json can be at the root of the ZIP or inside one top-level folder. Install it from Settings → Plugins → Add plugin.

greeter/
plugin.jsonmanifest: identity, permissions, settings
index.jsentry script: one ES module
style.cssoptional, needs ui

plugin.json

greeter/plugin.json
{
  "id": "greeter",
  "name": "Greeter",
  "version": "1.0.0",
  "author": "Your name",
  "description": "Says hello when Fermo opens.",
  "main": "index.js",
  "permissions": ["storage", "ui"],
  "compatibility": {"apiVersion": 1, "minVenoraVersion": "0.1.0"},
  "settings": [
    {"key": "greeting", "name": "Greeting", "type": "string", "default": "Hello!"}
  ],
  "changelog": [
    {"version": "1.0.0", "added": ["Says hello."]}
  ]
}

id, name and version are required. The id is permanent: updates, settings and storage are keyed by it. version follows semantic versioning, and an update must be higher. Put apiVersion inside compatibility. A top-level one is rejected, so it can't be silently ignored.

index.js

greeter/index.js
export default async (venora) => {
  const greeting = await venora.settings.get({key: "greeting"});
  await venora.ui.toast({message: greeting});

  const unsubscribe = await venora.on("settings.changed", ({key, value}) => {
    if (key === "greeting") venora.ui.toast({message: value});
  });

  return () => {
    // Cleanup: clear your own timers. Venora removes anything it added to
    // the page for this plugin, and ends its subscriptions, automatically.
    unsubscribe();
  };
};

02 The API

The venora object

Your default export is called with one argument, venora. It is created fresh each time the plugin starts, frozen, and not shared with any other plugin.

  • One argument. Every method takes at most one plain object. Unknown fields are ignored, including any pluginId.
  • Always a promise. Every call crosses the bridge, so every method returns a promise that resolves to a copy of plain data.
  • One method, one capability. venora.ui.toast(p) is exactly venora.request("ui.toast", p), with the same checks.
MethodPermissionsInput → resultOwn rate limit
venora.ui.toastui{message, durationMs?} → null. Plain text, up to 200 characters, shown for 1–10 s.5, then 1 / 2 s
venora.settings.getstorage{key} → the saved value, else the declared default, else nullnone
venora.storage.getstorage{key} → the stored value or nullnone
venora.storage.setstorage{key, value} → null60, then 20/s
venora.storage.removestorage{key} → null60, then 20/s
venora.storage.clearstorageno input → null; clears only your keys60, then 20/s
venora.mentions.showAvatarui account network{mentionId} → {shown}. Venora looks the avatar up; you never see the URL.100, then 50/s
venora.mentions.setAvatarSizeui{size}, 12–32 px → null10, then 2/s
venora.onper event(event, listener) → an unsubscribe function20, then 2/s

The object also has pluginId, apiVersion (1), and request(capability, payload), the low-level form of every method.

03 Events

Listening with venora.on

EventPermissionsPayloadSent when
settings.changedstorage{key, value}The user changes one of your settings while your plugin runs. Only you receive it.
mentions.addedui{mentions: [{mentionId, userId}]}Mentions appear on the page. New subscribers also get the ones already there, once each.

Delivery is at most once, in order, with no retries. Events that happen while you aren't subscribed aren't queued. Listeners can't cancel anything, and an exception in one is reported without affecting the others. Internal Venora events are never visible to plugins.

04 Permissions

Declare what you need, nothing more

A capability needs all of its permissions. Without one, the call rejects with permission_denied before your payload is even read. Permissions are fixed per installed version: there are no runtime prompts and no optional permissions. Adding a sensitive permission in an update means the user sees the warning again.

storagesettings and storage
uitoasts, CSS, mentions
networksensitive · avatar lookup only
accountsensitive · avatar lookup only

05 Settings & storage

The user's choices, and your own data

Settings

  • Declared in plugin.json: boolean, string, number (with min, max, step) or select
  • Written only by the user, in a dialog Venora draws. There is no settings.set.
  • Read them at start, then follow settings.changed

Storage

  • Keys of 1–100 characters; values are plain data
  • 64 KiB per value, 1 MiB and 1,000 keys per plugin, else quota_exceeded
  • Atomic, applied in call order, kept across reloads and updates, deleted on uninstall

Storage lives in the browser profile and is shared by every Fermo account used there, so don't assume data belongs to the current account. Each tab runs its own instance, and simultaneous writes from two tabs can lose one tab's change.

06 UI & CSS

Changing how Fermo looks

Name a stylesheet in "css" and declare ui. Venora applies it while your plugin runs and removes it when it stops. The CSS isn't applied if it contains url(, @import, image-set(, a backslash (among a few others), a quoted string other than "", or @counter-style, because those can load resources or write text onto the page. It also has to be under 131,072 characters. Your plugin still runs, and the reason is logged.

Plugin CSS applies to Fermo's page only. It can't reach Venora's dialogs, notices or Plugins page. There's no API for the DOM, dialogs or rendering HTML.

07 Lifecycle

When your code runs

  1. startingmodule loads; default export runs; API available
  2. runningexport returned or resolved
  3. stoppingcleanup runs; API calls get runtime_stopped
  4. stoppedframe removed; page additions removed
  • Plugins start on /channels/ pages and stop when Fermo navigates elsewhere. The same tab can start and stop your plugin many times, and each start is a fresh module instance.
  • Startup has 10 seconds. Start long work without awaiting it. Cleanup has 2 seconds.
  • Cleanup is not guaranteed: it doesn't run on reload, tab close, or when Venora terminates a plugin. Save data when it changes, not on the way out.
  • A plugin that fails to start shows failed with its error on its card and isn't retried until the next page load or navigation back to a channel.

08 Environment

What's available, and what isn't

Available

  • The venora object
  • Standard JavaScript built-ins, console and timers
  • crypto.getRandomValues, TextEncoder/TextDecoder, Intl

Blocked on purpose

  • fetch, XHR, WebSocket and loading any resource
  • window, document and navigation
  • localStorage, IndexedDB, cookies and chrome.*
  • eval, new Function and relative imports (bundle into one file)
  • alert, confirm, prompt and popups

09 Errors & limits

Handle error.code, not the message

Failed calls reject with an Error carrying a stable code. Messages are for humans and can change. Treat an unknown code as a general failure, because new codes may be added.

permission_denieda required permission isn't declared
invalid_payloadwrong type, out of range, or an undeclared setting key
rate_limitedover a request limit; nothing was done
quota_exceededa storage limit; nothing was written
timeoutno answer in 30 s; it may still have happened
runtime_stoppedcalled during or after cleanup
unknown_capabilitya typo, or a newer Venora's capability
unknown_eventnot a public event
sensitive_data_blockedthe result contained a session token
capability_failedan internal failure; details stay in Venora
invalid_messagethe request couldn't be read

Each plugin also has request, event and message limits, a watchdog, and storage and install size caps. A plugin that keeps exceeding them is stopped. The full table is on the security page.

10 Versioning

What API v1 promises

Within version 1, Venora may add capabilities, events, optional fields and error codes, and may raise limits. It won't remove or rename anything, add a permission to an existing capability, or lower a limit. Those need API version 2. The one exception is a breaking change needed to fix a vulnerability, which must be as small as possible and documented in the release notes.

When you use something added in a later Venora release, set minVenoraVersion so older releases refuse to install your plugin instead of failing at runtime.

11 Reference

Documentation in the repository