Making, packing and installing plugins

notesy plugin does everything around a plugin from a terminal, without opening a window: starting one, checking it, working on it with a running notesy, packing it into one file, and installing that file.

notesy plugin new you.word-goal      a new plugin folder
notesy plugin check                   everything wrong with it, with lines
notesy plugin dev                     link it into notesy; it reloads as you save
notesy plugin keygen                  your key, to sign your packages with (once)
notesy plugin pack                    you.word-goal-0.1.0.notesy-plugin, signed
notesy plugin signers add KEY         let a teammate's key (or your new one) sign its updates
notesy plugin install FILE            install a package
notesy plugin uninstall you.word-goal remove one (--keep-data keeps its data)

#Starting one

notesy plugin new you.word-goal makes a folder you.word-goal with a plugin.toml (plugin.toml), a main.rn that says hello in its log when it's ready and each time a note is saved, and a readme. notesy plugin new you.word-goal plugins/goal puts it elsewhere.

#Checking it

notesy plugin check (in the plugin's folder, or check FOLDER) reads the manifest, checks every file it names is there and that the folder can be packed, and compiles the script against exactly the modules its permissions allow. Every problem is listed, with the file and line for the script's. A script that uses a module it didn't ask for doesn't compile:

error: Missing item `notesy::status`
  ┌─ main.rn:1:5
  │
1 │ use notesy::status;
  │     ^^^^^^^^^^^^^^

(status takes the ui permission: add it to permissions.)

#Working on it

notesy plugin dev links the folder into notesy's plugins folder (a symbolic link, so you keep working where it is); Settings, Plugins, "Load a plugin folder" does the same from the window. Nothing is packed or signed. Its row on the Plugins page says where it's linked from and has a Reload button, and notesy looks at its files once a second: save any of them (a script, plugin.toml, a theme, an icon) and it's read again and starts afresh, saying so in its log, with every error and its line. Removing it only unlinks it: the folder stays where it is.

#Previewing it

notesy preview shows one screen of notesy for a look, and --plugin loads yours into it, straight from its folder:

notesy preview --plugin . plugin-page      its page in Settings
notesy preview --plugin . plugin-review    the prompt asking to let it run
notesy preview --plugin . view board       a kind of tab its script adds
notesy preview --plugin . panel tasks      a panel its script adds
notesy preview --plugin . palette          its commands, among notesy's

view and panel open once its script has added them. In a preview it's allowed what it asks for, for the look only: it runs over a few sample notes (never yours), keeps its data in a temporary folder and its secrets nowhere, and nothing it's allowed is saved. What reaches beyond the preview is held back: the network (and so signing in), reading the clipboard and opening links. Its script still compiles, each call is an error saying why, and its log says what's held back; --trust lets it have them. Your installed plugins don't run in a preview, and a preview installs, links and removes nothing. --plugin also takes an installed plugin's id, and can be given more than once. --record DIR saves the screen frame by frame, and notesy preview --list lists every screen.

A plugin can also name screens of its own in [[previews]], set up by its script (notesy::preview): notesy preview ana.tasks/board --plugin . shows one, and notesy preview --list lists them with notesy's.

#Signing and packing

notesy plugin keygen makes your key, once: an ed25519 key kept in notesy's config folder as signing.key, readable only by you. It prints the public half (what you give a teammate) and a short fingerprint for telling keys apart. Back it up. On a build machine, give the key in NOTESY_SIGNING_KEY (its 64 hex digits) instead.

notesy plugin pack checks the folder, then writes <id>-<version>.notesy-plugin in the folder you're in (--out DIR puts it elsewhere), signed with your key. --unsigned leaves the signature out; notesy still installs those, and says they're unsigned.

A signature says a package is exactly what its signer packed, and it's what keeps updates honest: once someone has your plugin, notesy takes an update of it only when a key it trusts for that plugin signed it. It doesn't say who you are; a key isn't a person, so notesy never shows one as a name. A registry will (plan.md).

#Who may sign its updates

A plugin starts with one signer: the key its first package was signed with. signers.toml, in its folder and packed with it, changes that. It's a log of keys added and removed, each change signed by a key that could sign the plugin's packages when it was made and linked to the one before it by its hash. notesy walks it from the keys it trusts, so a key comes in only through one already trusted, and a removed key can't sign again. notesy also remembers how far into the log it has read, and an update's signers.toml has to carry on from there: no change it took can be cut from the end or written again, so a key can't come back by leaving its removal out. Only ever add to the log; never start it over.

notesy plugin signers list          who may sign its updates now
notesy plugin signers add KEY       let another key sign them too
notesy plugin signers remove KEY    stop one (its key, or its fingerprint)

For a team: a teammate runs notesy plugin keygen and gives you the public key it prints; you add it and commit signers.toml, and from the next version either of you can pack and publish. A plugin keeps at least one signer.

For a new key of your own, while you still have the old one (it may have leaked, or it's simply time): notesy plugin keygen --rotate makes a new key and keeps the old one as signing.key.old; notesy plugin signers rotate in each of your plugins' folders adds the new key (signed by the old) and removes the old (signed by the new). Publish a version of each with its signers.toml, then delete signing.key.old: anyone with a version after the handover refuses updates signed with the old key. A key that's lost for good can't hand over; a teammate's can still remove it and add your new one, which is a reason to have two.

#What's in a package

A package is a zip:

Entry What it is
PACKAGE notesy-package 1, the plugin's id and version, then a line per file: its SHA-256, its size and its path
SIGNATURE format = "notesy-signature 2", then a [[signature]] for each: its role (author; a registry's will be registry), its public key and its sig, in hex, over notesy package 2, the role, and PACKAGE's SHA-256. (The first packages' ed25519 <key> and a signature over PACKAGE still read.)
everything else the plugin's folder, as it is

A signers.toml goes in like any file, so PACKAGE covers it, and it's checked whole when the package is read: it has to name the key the package is signed with. Files are sorted and stamped with a fixed time, so the same folder always packs to the same bytes. Hidden files and folders (.git) and packages in the folder stay out.

A package (and a folder being packed) can't have:

  • paths out of the plugin's folder (.., absolute paths);
  • links;
  • native code or things that run (.so, .dll, .dylib, .exe, .wasm, .sh, .bat, .ps1 …): plugins are Rune scripts and data;
  • more than 2,000 files, or more than 20 MiB unpacked.

#Installing

Drop a .notesy-plugin on notesy's window, pick one with Settings, Plugins, "Install a plugin", or run notesy plugin install FILE.

notesy reads the whole package first: every file has to be in PACKAGE with its size and hash, nothing else can be in it, the signature has to match, and its plugin.toml has to be the one PACKAGE names. Anything wrong and it isn't installed, and notesy says what.

Then it says whether it's signed, and by what: the same key as the version you have, a key that version handed over to, or a key new to notesy (which nothing vouches for yet); and whether it runs anything, to confirm. The key's fingerprint is under that, for comparing. It's unpacked into a folder beside the plugins and moved into place in one step, so a failed install leaves nothing behind. Its PACKAGE and SIGNATURE stay beside its files, and the Plugins page shows it as signed only while its signature holds and every file is still the one signed. A plugin that runs a script then asks for what its manifest declares, as any plugin does, before it runs.

#Updating

Installing a newer version of an installed plugin replaces it and keeps its data. The first time a plugin is installed from a signed package, notesy remembers the keys that may sign it (the one it was signed with, or the ones its signers.toml names) and how far into that log it read, in plugins/grants.toml. An update has to be signed by one of them, as its own signers.toml leaves them (carrying on from where notesy read to), or it's refused, saying why; an unsigned update of a signed plugin is refused too. An update that asks for more than before asks again ("Review update").

An older version isn't installed over a newer one: it could bring back what the newer one fixed. Remove the plugin first to go back, or run notesy plugin install --downgrade FILE.

#Removing

Settings, Plugins, a plugin's "Remove" (or notesy plugin uninstall ID) stops it, takes away everything it added, and removes its folder. It asks whether to keep its data (its store, and its secrets in the system keychain) for next time, or remove that too; notesy plugin uninstall removes both unless --keep-data.

Every page