Modules
Publishing
Signing
[signing].key_file in gansey.toml (or GANSEY_SIGNING_KEY) names a 32-byte ed25519 seed the site lends to modules with sign = true. The key stays in its file (mode 600; the site will not read a key others can) and is never handed to a script: a module can ask for the public key and for a signature over one of its stored files. gansey signing-key new --out <file> makes one; gansey signing-key show prints the public half. Without a key, ctx.sign returns Err and the site still starts.
Switching a module off
Modules → a module → the switch, or gansey mod disable <id> / gansey mod enable <id>. A switched-off module stays in its folder with its tables, but init does not run, so nothing it registers is active: its routes return 404, its widgets drop out of the builder, its admin screens, hooks and jobs stop. File changes do not bring it back. The list of switched-off modules is stored per site in the settings row modules.disabled, so a restart keeps the choice. The CLI writes the same row; a running server picks the change up the next time it reloads the module (or restarts).
Installing from the registry
gansey mod add <registry-id[@version]>and Modules → Registry install packages from the hub; paid packages need the site's licence key.- A package from the registry is checked before anything unpacks it: its SHA-256 and its ed25519 signature must match the hub's signing key, the same key the site pinned for core updates (
[updates].public_key, or the hub's/api/v1/releases/public-keyon first use). A package that fails is discarded. The install route'surl(a direct download from the hub) needs the hub'sX-Checksum-Sha256andX-Signature-Ed25519headers; anything unsigned is refused there. The licence key goes only to the hub and the downloads its registry lists, never to an address typed in. gansey mod remove <id> [--purge]removes the folder, and with--purgeits tables, store and queue.
Your own modules
A module does not have to come from the hub. Anyone can write one and share it as a folder, a package file, a git repository or a download address, and a site can install it without us:
gansey mod add ../opening-hours # a folder
gansey mod add opening-hours-1.0.0.tar.gz # a package file
gansey mod add https://example.test/you/opening-hours.git#v1.0.0
gansey mod add [email protected]:you/modules.git --ref main --path opening-hours
gansey mod add https://example.test/opening-hours-1.0.0.tar.gz
- A repository is any
https://,ssh://orgit://address, git'suser@host:path, or anything aftergit+.#branch-or-tagor--refpicks the branch, tag or commit (the default branch otherwise);--pathnames the folder holdingmodule.tomlwhen it is not at the top. The server runsgit clonewithout prompting, so it needs git, and a private repository needs a key or credentials set up for the user the site runs as. Links in a repository arrive as plain files. - A folder is packed as
gansey mod packwould, so hidden files such as.gitstay behind. A package file or download may hold the module folder at its top or one level down (a repository host's download of a tag puts it there); it is rebuilt from its plain files, and one holding links or more than one module is refused. - None of these is checked against the hub's key (a download the hub serves with its signature headers is), so the command says the package goes in on your say-so. Core still holds the module to its manifest's permissions.
- In the admin, Modules → Add your own takes an uploaded package, a repository (with an optional branch, tag or commit and folder) or a package URL. The package is fetched and set aside, and the screen shows its name, version, where it came from, the core it needs, every permission it asks for (marking any the installed version does not have) and whether the hub signed it. A second click installs it; nothing is installed before then, and a package left waiting more than an hour is cleared away the next time one is fetched. The admin only clones network addresses, never a path on the server.
- Each install records where it came from in the module's folder (
.origin.json), and the Modules screen shows it: a module that was not signed is markedunsignedand listed under Yours, whatever its manifest'ssourcesays. [modules].allow_unsigned = false(GANSEY_MODULES_ALLOW_UNSIGNED) keeps a site's admin to packages its hub signed: Add your own disappears and the routes below refuse anything unsigned. A host sets it for sites it runs. The command line is not affected.- The routes behind the screen, all needing
manage_modules:POST /api/modules/fetchwith{ git, ref?, path? }or{ url }, andPOST /api/modules/uploadwith a multipartfile, answer{ token, package, origin, replaces };POST /api/modules/installwith{ staged: token }installs it, once. The activity log records the install with where it came from and whether it was signed.
Packing and publishing
- A package for the registry is a gzip tarball of the module folder;
gansey mod pack <folder> --out <dir>writes<id>-<version>.tar.gzand prints its SHA-256. Packing is deterministic, so the same folder always gives the same checksum. gansey mod publish <folder>packs the folder and sends it to the hub (--hub,GANSEY_HUB_URLor[updates].hub_url) with the publish token from the hub's Packages screen (--tokenorGANSEY_REGISTRY_TOKEN). Release CI sends its OIDC token instead (trusted publishing): the hub checks it came from the shared module release workflow, for the tagv<version>, in the repository that first published the module. The hub reads the version, core range and permissions from the package's ownmodule.toml, hashes and signs it; the command checks the stored checksum and the signature (againstGANSEY_RELEASE_PUBLIC_KEY, or the hub's published key) before it succeeds. A version the registry already has is refused: bumpversion.--if-newerskips quietly instead, unless the version is newer than every one the registry holds, for scripts that publish whatever a folder holds. Official modules are published by av<version>tag in each module's own repository (gansey-module-<id>).