Venora is trusted code
When you install the extension, you trust it. It runs inside Fermo's page and can read that page and Fermo's local account data, which it needs in order to guard your token and to do work on plugins' behalf.
Security & privacy
Venora is designed to reduce the trust and privilege a plugin needs, rather than treating every plugin as fully trusted code. This page explains how it does that, what it stores, and where its protections stop.
01 Trust model
The trust boundary is each plugin's sandboxed frame and its message channel. Inside, the plugin controls everything. Outside, Venora validates everything that crosses.
When you install the extension, you trust it. It runs inside Fermo's page and can read that page and Fermo's local account data, which it needs in order to guard your token and to do work on plugins' behalf.
Imported plugin code and manifests are treated as untrusted. Bundled plugins get no exceptions: they run in the same sandbox, with only the permissions they declare.
Plugin frames sit inside a closed shadow root, and each private channel is handed over once with a random nonce. Fermo's own scripts cannot obtain a plugin's channel.
02 Architecture
Each layer removes something a plugin could otherwise reach. Scroll through them in order, from the plugin's own code out to the browser toolbar.
Plugin code has no window, document, parent or assignable location. It cannot touch any DOM, and it cannot navigate a frame to smuggle data out in a URL.
It gets the standard JavaScript built-ins, timers, console and a few web-platform basics. eval and new Function are blocked. The code is a single ES module loaded from a data: URL.
The worker is created by a sandboxed extension page with an opaque origin and no chrome.* APIs. Its content security policy is default-src 'none', so fetch, XHR, WebSocket and loading images, scripts or frames are blocked. The worker inherits this policy, which the browser test confirms in Chromium.
An opaque origin also means no access to Fermo's localStorage, IndexedDB or cookies, which is where Fermo keeps your session.
Frames sit in a closed shadow root. At start, Venora posts a connect message with a random 128-bit nonce and one transferred MessagePort. The sandbox accepts exactly one, and only from its parent.
Venora decides which plugin is asking from the port a request arrives on. A request that includes a plugin ID is rejected. Only plain, JSON-compatible data can cross, within size limits.
The registry is fixed at load. The dispatcher checks that the capability exists, that every permission it needs was declared, and that the payload is valid, in that order. Permissions are copied from the validated install record when the plugin starts, so nothing the plugin sends can change them.
Internal exceptions come back only as a generic capability_failed. Stack traces, paths and server responses are never passed to the plugin.
Capabilities that need your session use it inside Venora and return only a derived result. Then, as a backstop, every message to a plugin passes through one function that checks it against your current tokens, for every Fermo account in the profile, including inside longer strings.
A response containing a token becomes sensitive_data_blocked. An event containing one is dropped. If the tokens can't be read, nothing is sent. The guard never logs the token.
Requests waiting for an answer, request rate, each costly capability's rate and event rate are all counted per plugin. Over a limit, requests are refused with rate_limited before any work is done.
A watchdog pings each worker every 2 seconds and stops one that hasn't answered within 5, removing its frame and worker.
Dialogs, notices and the Plugins page render in shadow roots on hosts with inline !important styles, and dialogs and notices use the browser's top layer. Plugin CSS cannot restyle, hide or cover them.
Every plugin-provided string is rendered as text, never HTML. Imported plugins' notices carry a fixed “Plugin” badge and the plugin's name, so a plugin calling itself “Venora” still can't pose as Venora.
Venora runs only in Fermo's top-level page at fermo.sbar.fyi. It never runs in a framed copy of Fermo or on a look-alike origin. Plugins run on /channels/ pages, and the page context is checked again before every start, request and event.
If Fermo leaves /channels/ or its document is replaced, requests are refused with runtime_stopped and the plugins are stopped.
Plugin CSS can restyle Fermo's own page, including the settings area. So recovery lives in the Venora toolbar popup, an extension page that no plugin CSS can reach. From it you can turn on safe mode, which stops every imported plugin, or enable, disable and uninstall plugins. Open Fermo tabs apply the change at once.
03 Permissions
A permission is consent. On its own it grants nothing. It matters only because capabilities and events require it. Permissions come from plugin.json, are fixed for each installed version, and are checked on every call. An unknown permission blocks installation.
| Permission | Lets the plugin cause | Never gives the plugin |
|---|---|---|
storage | Reading its own settings and storing its own data | Other plugins' settings or data; Venora's or Fermo's storage |
ui | Notices, its declared CSS, mention avatars, the mentions.added event | Direct DOM access; page content beyond what an event reports; any change to Venora's UI |
network sensitive | Venora making web requests on its behalf, inside capabilities that require it | A way to send its own requests, or to see responses beyond what the capability returns |
account sensitive | Venora acting with your signed-in Fermo account on its behalf | The session token, the account record, credentials, or profile data |
A permission is sensitive if it lets a plugin cause Venora to act on your behalf, send data off the device, or use your identity. Before installing, and before every update that declares one, Venora shows a warning listing them in its own top-layer dialog, and the plugin is stored only if you confirm. Anything that can't ask you fails closed.
In API v1, network and account are consent markers. No capability performs arbitrary requests or returns account data. They are needed only for the avatar lookup.
An imported ZIP is checked in full: structure and paths, the manifest, version, permissions, compatibility, changelog, settings, and file sizes. If anything fails, nothing is saved. Fields that only Venora may set, such as required and enabled, are stripped from imported manifests, so a third-party plugin cannot make itself impossible to disable.
04 Storage isolation
Settings and storage are addressed by the plugin ID Venora bound to the running plugin. No method takes a plugin ID, and there is no way to list keys or other plugins. A plugin can't read, write or detect another plugin's settings or storage, or even tell whether that plugin exists.
Each plugin's data is one entry in extension storage, separate from Venora's own records and from Fermo's storage, with quotas checked before every write. Uninstalling an imported plugin deletes its data.
05 Runtime limits
| Resource | Limit | When exceeded |
|---|---|---|
| Requests waiting for an answer | 64 per plugin | rate_limited; nothing is done |
| Requests, all capabilities | 120 at once, refilled at 60 per second | rate_limited |
| Costly capabilities | Their own limit, e.g. ui.toast: 5 at once, then 1 every 2 s | rate_limited |
| Events to a plugin | 100 at once, refilled at 50 per second | Event dropped |
| One message | 50,000 values; 1,000,000 characters of strings and keys | Plugin stopped |
| Unanswered capability | 30 seconds | timeout; slot freed |
| Unresponsive worker | Pinged every 2 s; no answer within 5 s | Plugin stopped; frame and worker removed |
| Startup / cleanup | 10 seconds / 2 seconds | Failed / frame removed anyway |
| Sustained rejections | More than 200 rate_limited in 10 s | Plugin stopped |
| Repeated problems | 3 stops for misbehaviour on one page | Not restarted until reload |
| Storage | 64 KiB per value; 1 MiB and 1,000 keys per plugin | quota_exceeded |
| Install | ZIP 16 MiB; manifest and CSS 131,072 characters; entry script 2,000,000 | Not installed |
One plugin's failure doesn't stop another: accounts are per plugin, and a failed plugin's frame and page additions are removed while the others keep running.
06 Recovery
07 Privacy
Venora has no server and no account system. Its code contains no analytics or telemetry, and it syncs nothing. Here is what each party can see and keep.
The extension asks the browser for storage and for access to https://fermo.sbar.fyi/* only. In Fermo's page it reads the page to find mentions, and Fermo's local account data to guard your token and to look up avatars.
Only what capabilities return: its own settings and storage, and, with ui, the opaque IDs of mentions on the page with the mentioned users' public user IDs. Avatar requests return only whether an avatar was shown.
In this browser profile's extension storage: imported plugin files and manifests, which plugins are enabled, safe mode, plugin settings, and each plugin's own storage. It is not synced between devices, and it is shared by every Fermo account used in that profile.
Nothing goes to Venora. When a plugin with the ui, account and network permissions shows a mention avatar, as the bundled Mention avatars plugin does, Venora may request that user's profile from the Spacebar server your Fermo account uses, and the avatar image loads from that server.
This website has no analytics, sets no cookies, and serves its fonts itself.
08 Current limitations
These are known, documented gaps. Venora would rather list them than leave them for you to discover.
ui, CSS can restyle or hide parts of Fermo, including the settings area. It can't load resources, write text, or reach Venora's UI or the popup, and safe mode removes it at once.09 Verify it yourself
The security requirements are normative: a change that breaks one is treated as a security bug. Each lists the tests that cover it. The unit tests need only Node.js 18 or later, and the browser test needs Chrome or Edge 126 or later.
$ node --test tests/*.test.js
$ node tests/browser/chromium-e2e.mjs