Skip to content

Plugins and the plugin catalog

Modalities and annotation types are plugins: the built-in ones (ygg.*) ship with the app, and an installation can add others from its plugin catalog. A superadmin adds a plugin package in the console’s Plugins section and enables it; every member’s app then loads it when it starts, and the plugin’s annotation types appear in the setup wizard beside the built-in ones.

What a plugin can do, and what it is trusted with

Section titled “What a plugin can do, and what it is trusted with”

A plugin is code that runs in every member’s browser with the app’s privileges. It sees what the app decrypts on that page (the images and answers a member opens), as the built-in plugins do; it never sees private keys, which stay in the app’s crypto worker. Enabling a plugin is therefore trusting its code like the app’s own: enable only code you reviewed, from people you trust.

What the platform guarantees:

  • The bytes are the ones you enabled. A package names its module’s Subresource Integrity hash (SHA-384). The catalog refuses a module that does not match, and browsers refuse to run one that does not. The hash does not protect against the server itself, which serves the whole app.
  • No code from text. The app’s Content Security Policy allows scripts from its own origin only, and never eval. The catalog serves each module from the app’s origin, so plugins run under the same policy; a plugin that compiles code at runtime, or loads scripts from elsewhere, does not work.
  • Namespaces. A package’s ID is outside the reserved ygg namespace, and everything it provides lies under that ID (org.example.rating provides org.example.rating@1 or org.example.rating.stars@1), so two packages never provide the same plugin.
  • Exactly what it declares. The app loads a plugin only when its module registers exactly the plugins its manifest lists, and each passes the plugin contract’s checks. A plugin that fails to load is left out, and the console says why; the app starts without it.

The server stores the catalog as plaintext instance metadata (programs, not patients) and records every addition, enabling, disabling and removal on the instance’s audit chain.

A package is two files:

File What it is
yggdrasil-plugin.json The package manifest (schema plugin/package-v1): ID, version, name, description, author, licence, homepage, the plugin API version it is built against, the module’s file name and integrity, and the refs it provides.
plugin.js One ES module (at most 4 MiB) whose default export is a plugin factory. It imports nothing at runtime: React, React Intl, the design system and the plugin API’s runtime come from the app.
yggdrasil-plugin.json
{
"id": "org.example.rating",
"version": "1.0.0",
"name": "Star rating",
"apiVersion": "1.6.0",
"module": "plugin.js",
"integrity": "sha384-…",
"provides": { "annotationTypes": ["org.example.rating@1"] }
}

The catalog holds one entry per package ID and version. Enabling a version disables the package’s other versions.

The plugin API is the package @yggdrasil/plugin-api (its README), versioned on its own with semantic versioning: PLUGIN_API_VERSION, now 1.6.0. A plugin names the version it was built against in apiVersion; an app loads it when its own plugin API has the same major and at least that minor. A minor release only adds to the contract; a major release breaks it, and plugins are rebuilt against it.

  1. In the console, open Plugins and choose Add a plugin package. Pick both files of the package.
  2. Check the details: the ID, the version, what it provides and its integrity. Add, disabled.
  3. Enable it. Members’ apps load it from their next start; reload the app to load it in yours. The entry then says Loaded in this browser, or why it was not loaded.

Disabling a plugin stops loading it. Annotations its types wrote stay, encrypted like any other, but no app can show or edit them until the plugin is enabled again. Only a disabled plugin can be removed.

  • A plugin’s components use the design system’s components and the app’s styles: the app ships no styles for class names it does not use itself.
  • Strings: plugins supply their own English text through React Intl message descriptors; the app’s translations do not cover them.
  • The server cannot check a plugin’s annotations (it never sees them decrypted). Exports carry them like any answer, with the plugin’s ref.
  • A modality plugin’s ingest worker runs without an integrity check, like the app’s own workers: browsers take none for workers.