Every page was read against the code on dev. What changed: - Pages that described 4.2 behaviour: satellite view on the vector maps, the mobile journal timeline, Collab with its fifth tab, the Review badge, clustered pins on the share page, the Leaflet preset list, the strict guard on fe80::/10, the invite link tools and their scopes, the Public API scope rules, the permission count in the plugin SDK. - Features of this release that no page mentioned: videos in a journal entry, receipts on an expense, the Google transit backend, routing counters, the rating on a place, the navigation menu, car stops, the manual school holiday catalogue and its MCP tools, the Dawarich panel in the Atlas, the release notes modal, plugin frames going fullscreen, the corridor call into the search provider hook. - Statements that sent an admin the wrong way: DEFAULT_LANGUAGE is not in the Helm ConfigMap, env.PORT cannot be changed on the chart alone, the rotation script does not cover the document sync credentials, event handlers cannot broadcast, there is no per user MFA reset in the admin panel, the demo user can edit, an unparsable APP_URL stops the server. - Updating gets a section for 4.3.0: the TREK Places API as the new default, the new variables, the Overpass timeout, SSO sessions on an OIDC only instance, the migration that puts booked nights on the drive.
22 KiB
Publishing a Plugin
Plugins are distributed from a static registry — the
TREK-Plugins GitHub repo. There is
no upload server and no account: you host the code in your own public GitHub repo,
attach a built plugin.zip to a release, and list it with a pull request.
The trek-plugin CLI (shipped in the trek-plugin-sdk package) does almost all
the mechanical work — you rarely hand-type a hash, size, commit, or JSON field.
The short version — one command
Get the plugin green locally (trek-plugin status will tell you what is left), commit
and push it to its public GitHub repo, then:
npx trek-plugin-sdk publish --repo you/trek-plugin-flight-tracker --tag v1.0.0
publish runs the whole release, in five steps and in this order:
- check — every registry gate that can be checked locally
- pack — build
plugin.zip - release — git tag, push, and cut the GitHub release with the artifact attached
- preflight — the gates that need the tag and the release to exist
- submit — open the PR against the registry
The order is the point. A GitHub release is effectively immutable: the registry pins its sha256, so overwriting the bytes breaks the checksum for everyone who already installed that version. So the local gates run first — if one fails, nothing is packed, tagged, pushed or released, and you can fix it and re-run against the same version.
And when a later step fails — a preflight or PR problem after the release is
cut — publish rolls back exactly what that run created: the GitHub release, the
pushed tag, and the local tag, so the same tag is free to re-run against once you
have fixed the problem. Anything that existed before the run is never touched, and
--keep-release opts out (the error then prints the manual cleanup commands). A
leftover tag from an earlier failed run — one with no release that no longer points
at HEAD — is recognised as debris and moved to your current commit rather than
silently releasing the stale one. And for a stranded state you already have,
trek-plugin unrelease <vX.Y.Z> --repo you/repo deletes the release, the remote
tag, and the local tag in one go — after checking the published registry index and
refusing, with no override, to touch a version that is actually published (its
artifact is immutable; ship a new version instead).
It prints the PR URL at the end. In a terminal it offers to sign the release (and
creates the key if you have none); scripts and CI are never prompted and pass --sign.
--no-checks skips step 1 and
--no-preflight skips step 4 — escape hatches for a re-run, never right on a first
publish. It needs git and an authenticated gh.
The individual steps below still exist for when you want one by hand — release
(pack → GitHub release → entry), preflight, and submit (opens the PR) — or
entry --out registry/plugins/<id>.json to write the file and open the PR
yourself.
1. Host and build your plugin
Put your plugin in a public GitHub repo (convention: trek-plugin-<id>).
create-trek-plugin scaffolds the layout:
npx trek-plugin-sdk create flight-tracker --type widget # integration | page | widget | trip-page
A publishable plugin has, at the repo root:
trek-plugin.json— the manifest (see Plugin Development)package.json— the CommonJS marker ("type": "commonjs"), with the SDK as a devDependency at mostserver/index.js— the built server entry (required)client/— the built frontend (only forpage/widget/trip-pageplugins)README.md— filled in, with a real screenshot (the quality gate is strict — see below)docs/screenshot.png— the store card image, committed to the repo
What create gives you runs and packs, but it does not pass validate: the README
is a template and there is no screenshot. Those are the two things only you can write.
2. Get it green locally
npx trek-plugin-sdk status # where am I? what's left? — never fails
npx trek-plugin-sdk validate . # the same checks, but it exits non-zero
status and validate run the same registry gates at two different depths.
status prints them as a checklist grouped by stage (Manifest, Code, Docs, Release,
Repo) and names the one command to run next; it is for orientation, so it never fails.
validate is the gate — same checks, exit code — and is the form for scripts and CI.
Between them they now catch, offline, nearly everything the registry rejects for:
- an
iconthat isn't a real lucide name (TREK falls back toBlockssilently, so a typo is invisible locally — but CI rejects it) - a README missing any of the four required sections, still carrying scaffold placeholders, or under 400 characters of real prose
- a missing
docs/screenshot.png— the registry fetches exactly that path at the pinned commit (it is what the store card loads), so a README link to any other image name doesn't count; the file has to be there - a permission your manifest declares but the README never explains
- a
name,descriptionorauthoroutside the registry's length limits, or a name that mixes Latin with Cyrillic/Greek look-alikes (a homoglyph spoof) - an
egress[]host with no matchinghttp:outbound:<host>permission — TREK builds the network allow-list and the iframe CSP from those permissions only and never readsegress[], so such a host is silently unreachable at runtime - a permission TREK doesn't know — the SDK and registry CI check against the same
65-entry list the host enforces, so a typo'd permission fails here instead of at
install on every instance (the registry's copy is a manually regenerated snapshot,
so a brand-new TREK permission can briefly fail registry CI even though
validateaccepts it — that's the snapshot lagging, not a typo)
Only six gates genuinely need the network, because none of them can be answered from
your working tree: that the tag resolves to the commit the entry pins, that the manifest at
that commit matches the entry, that the README at that commit still passes the quality gate,
that the released artifact downloads and hashes to the pinned sha256 (and verifies against
your key), that your plugin id isn't bound to another GitHub owner, and that an update
doesn't drop or rotate a signing key you already published under. Those run in preflight
— step 4 of publish.
Capture the screenshot
npm i -D playwright && npx playwright install chromium # once — not an SDK dependency
npx trek-plugin-sdk shot # --dark for the dark theme
shot boots the dev server, renders your plugin in the same themed frame TREK uses,
and writes a 1600×900 docs/screenshot.png. An integration plugin has no UI to
render, so shot can't help — screenshot the TREK surface your plugin changes instead.
Commit the screenshot. The registry resolves it at the pinned commit, so an image
that exists only in your working tree fails CI even though status was green.
3. Pack
npx trek-plugin-sdk pack . # writes ./plugin.zip
npx trek-plugin-sdk pack . --out dist.zip # custom output path
npx trek-plugin-sdk pack . --json # machine-readable result
pack builds plugin.zip in the installer's exact layout and prints the sha256
and size you'd otherwise compute by hand. It refuses to pack a plugin that could
not load — a broken manifest, a missing server/index.js, a native binary — but it
deliberately does not enforce the publish gates (an unwritten README, a missing
screenshot), because packing is also how you sideload a plugin into a local TREK to try
it, and the docs only have to be there when you ship. validate is what gates those.
It ships
only what the runtime needs — trek-plugin.json, README.md, LICENSE,
package.json, and the server/ and client/ trees — and drops node_modules,
.git, source maps and .ts sources. It refuses native binaries
(.node, binding.gyp, prebuilds/) and enforces the same size limits as the
installer (25 MB per file, 50 MB total, 4000 entries).
docs/is intentionally not shipped. The store fetches yourdocs/screenshot.pngstraight from the repo at the pinned commit, so keep it committed to GitHub but out of the zip —packhandles this for you.
The plugin.zip pack produces is also the artifact for sideloading: hand it
to an instance admin (or drag it onto Admin → Plugins) to install without the
registry at all — no PR, no review, no SHA-256/signature pin. Sideloaded plugins
are flagged as such and never auto-update, so the registry PR below is still the
path for anything you want discoverable and updatable. See Plugins.
4. Create the GitHub release
Tag vX.Y.Z where X.Y.Z equals version in your manifest, and attach the
packed plugin.zip as a release asset:
gh release create v1.0.0 plugin.zip --repo you/trek-plugin-flight-tracker
Prefer the uploaded plugin.zip asset — it's the exact bytes you packed and the
registry pins their hash. Don't rely on GitHub's auto-generated source archives;
they aren't the installer layout and their bytes aren't stable.
5. Generate the registry entry
npx trek-plugin-sdk entry \
--repo you/trek-plugin-flight-tracker \
--tag v1.0.0 \
--out registry/plugins/flight-tracker.json
entry reads your manifest and plugin.zip and emits the complete entry —
deriving commitSha (from git rev-parse <tag>^{commit}), downloadUrl,
sha256, size, apiVersion, and trek — your manifest's range, verbatim.
That range is the entry's only compatibility field: it is what TREK gates installs
and activation on. (The older minTrekVersion/maxTrekVersion are deprecated
and no longer emitted — the first merely restated the range's lower bound, and the
second is inclusive, so it cannot express <4.0.0 at all. Entries published
before trek existed may still carry them.) entry refuses to build an entry for a
manifest with no usable trek range, because TREK would refuse to install it. Flags: --zip
(default plugin.zip), --commit <sha> to override commit resolution, --asset
to name a differently-named release asset, --merge for updates (below), and
--out to write a file.
One-shot: release
npx trek-plugin-sdk release . --repo you/trek-plugin-flight-tracker --tag v1.0.0
release does pack → gh release create → entry in one go and prints the
entry to stdout. It accepts --out, --notes, --commit, and --merge. (It
requires the gh CLI, authenticated.)
6. Preflight — the checks that need the release to exist
Before you open the PR, run the registry's remaining CI checks against your pushed release, so you catch what CI would reject without a review round-trip:
npx trek-plugin-sdk preflight --repo you/trek-plugin-flight-tracker --tag v1.0.0
preflight runs everything validate runs, plus the six gates that genuinely need
the network: the tag resolves to the pinned commitSha; the manifest at that commit
matches the entry; the README at that commit passes the quality gate; the released
artifact downloads, hashes to the pinned sha256, carries no native binaries and
verifies against your key; your plugin id is not bound to a different GitHub owner; and
an update does not drop or rotate a signing key you already published under
(--allow-key-change declares a deliberate rotation — see "Rotating your signing key").
Re-grading the manifest and the README at the commit is not redundant with the local
pass: an author who writes the README and forgets to commit it has a green tree and a
red tag, and the tag is what CI grades. --entry <file.json> checks a hand-written
entry, --all checks every version rather than just the newest. A green preflight
predicts a green CI.
7. Open the registry PR
The fast path — submit does the whole fork/branch/commit/PR dance for you:
npx trek-plugin-sdk submit --repo you/trek-plugin-flight-tracker --tag v1.0.0
It forks TREK-Plugins (once),
fast-forwards your fork from the registry (a diverged fork warns with the
gh repo sync --force command to reset it, and the submit continues), branches off
the registry's current main, writes (or, for an update, merges into)
registry/plugins/<id>.json, pushes, and opens the PR — printing its URL. Add
--draft for a draft PR, --registry <owner/name> for a mirror. (Requires gh,
authenticated.)
By hand instead: fork the registry, add your generated file as
registry/plugins/<id>.json, and open a PR back to main. Add only that
file — dist/ is generated on merge, and CI rejects manual edits to it.
The entry follows schema/plugin-entry.schema.json;
schema/example-entry.json
is the canonical shape. size is required (a common omission), as are
commitSha, downloadUrl, sha256, trek, apiVersion, and
nativeModules: false on every version — all of which trek-plugin entry fills
in for you. CI rejects an entry whose trek disagrees with the manifest at
commitSha (and, on a legacy entry that still carries a minTrekVersion, one
whose floor disagrees with that range).
What CI enforces
CI runs scripts/validate-entry.mjs and scripts/check-readme.mjs on every
changed registry/plugins/*.json. Nearly every rule below is a pure function of your
trek-plugin.json and README.md, so trek-plugin validate checks it offline before
you have tagged anything — you should never learn any of this from CI.
Entry (validate-entry.mjs): valid schema · id matches the filename and
the slug pattern ^[a-z][a-z0-9-]{2,39}$ and is not one of the reserved ids
registry/install/rescan (they collide with admin API route segments — CI and
the install loader both refuse them) · your id is bound to your GitHub
owner on first registration, so nobody can repoint it later (owner changes need a
maintainer override) · homoglyph/mixed-script name check · versions[] sorted
newest-first · the release tag exists
and resolves to commitSha · manifest parity at that commit (id, version,
type, apiVersion, and nativeModules must not be true) · every declared
permission is on TREK's known-permission list (or a valid http:outbound:<host>) ·
the downloaded
artifact's SHA-256 matches the pin and its size is within bounds · no native
binaries in the archive · egress[] present (and no bare *) when
http:outbound is declared.
README (check-readme.mjs, fetched at the pinned commit): must exist at the
repo root, contain the sections What it does / Screenshots / Permissions /
Setup, and docs/screenshot.png must resolve to a real image at that
commit — the gate fetches exactly that path, because it is what the store card
loads; README links to other image names do not satisfy it. The README must also
have real prose (≥ 400 characters after stripping headings/code/images/tables),
contain no leftover scaffold placeholders, and explain every permission your
manifest declares (each permission string must appear in the README).
Provenance & integrity
commitShapins the exact source the maintainer reviewed (git tags are movable).sha256pins the exact artifact bytes TREK will run (release assets are mutable).
TREK verifies the downloaded bytes against sha256 and refuses to install on a
mismatch. A reviewedAt date on your entry means a maintainer looked at that
exact commit — it is not an ongoing guarantee. reviewedAt and boundOwner
are maintained by CI on merge; don't set them yourself.
Signing your releases (optional, recommended)
sha256 proves the bytes are the ones the registry vouches for. An author
signature additionally proves the bytes were signed by you, so a compromised
registry can't ship attacker code under your name. The entry schema allows two
optional fields — authorPublicKey on the entry and signature on each version.
TREK verifies the signature offline (minisign / Ed25519, no external service) and
pins your key on first install (trust-on-first-use): a later release signed with a
different key is refused until an admin re-trusts it.
The easy way — let the SDK do it (dependency-free Ed25519, no minisign needed).
In a terminal, publish proposes signing: it explains the tradeoff, offers to
create the key if you have none, and signs. You never have to know keygen or
--sign exist. Scripts and CI are never prompted — they pass --sign:
npx trek-plugin-sdk publish --repo you/repo --tag v1.2.0 --sign
--sign signs the exact artifact bytes and fills both authorPublicKey (entry)
and signature (version) for you. If the plugin was already published signed,
publish refuses an unsigned release at step 1 — and, since SDK 1.7.0, one
whose signing key's identity differs from the published authorPublicKey —
before anything is packed, tagged or released, because a GitHub release is
immutable and learning this at step 4 would have burned the tag. Back up
~/.trek-plugin/signing.key — losing it means a full key rotation (see below),
which strands every install until its admin re-trusts you.
By hand with minisign, if you prefer:
minisign -G # writes minisign.key (keep secret) + minisign.pub
Put the base64 payload line from minisign.pub in your entry as
authorPublicKey (stable across versions), then per release:
minisign -Sm plugin.zip # writes plugin.zip.minisig
Add the base64 line from plugin.zip.minisig to that version as signature,
next to its sha256:
{
"id": "flight-tracker",
"authorPublicKey": "RWQ…base64 minisign public key…",
"versions": [{
"version": "1.2.0",
"sha256": "3b2a…",
"signature": "RUR…base64 .minisig payload…"
}]
}
Signing is opt-in, and it is a one-way door you may walk through late. An
entry without authorPublicKey/signature installs on sha256 alone; going
unsigned → signed later breaks nobody, because nothing is pinned until a signed
version installs — adding a key at v1.4.0 is a real option, not a lost cause. The
registry does require every version signed once your key appears in the entry, and
the SDK handles that for you: your first signed update retro-signs the older
versions (each pinned artifact is downloaded, verified against its sha256, and
signed with the same key — a byte mismatch refuses, since that artifact no longer
matches its pin). What
you can never do is go back: once a plugin has shipped signed, an unsigned update
is refused (SIGNATURE_MISSING) on every instance that already has it, and a key
rotation needs a registry maintainer override (allow-key-change) plus an admin
re-trust on each instance — see the next section. So sign whenever you like — but
back the key up.
Rotating your signing key
A rotation — lost key, compromised machine, planned hygiene — is the one signing
change with a sanctioned path, and since SDK 1.7.0 the SDK drives its whole
artifact half. Two flows, one rule: a rotated entry must have every version
re-signed with the new key, because TREK verifies whichever version it installs
against the entry's single authorPublicKey.
Without shipping a version:
npx trek-plugin-sdk rotate-key # from the plugin dir; --id <plugin-id> from elsewhere
# --key <file> for a key not at ~/.trek-plugin/signing.key
# --out entry.json writes the rotated entry for a hand-made PR instead of opening one
It fetches your published registry entry, downloads every pinned artifact,
verifies each against its pinned sha256, re-signs all of them with the new key
(all-or-nothing — one unfetchable or tampered artifact aborts the whole rotation),
swaps authorPublicKey, and opens a registry PR titled as a rotation. (If the key
was merely lost, keygen first — the default path is free again. A
compromised key still on disk needs --key for the new one; keygen refuses to
overwrite.)
As part of a release:
npx trek-plugin-sdk publish --repo you/repo --tag v1.3.0 --sign --allow-key-change
Step 1 accepts the new key, the update merges onto your existing entry with the
older versions re-signed under the new key, and the PR title carries
"(key rotation)". The same --allow-key-change flag exists on
entry/preflight/submit/release for hand-assembled flows.
Either way, the SDK can only do the artifact half. The PR body says the rest out loud, and neither step is a formality:
- a registry maintainer must apply the
allow-key-changelabel — CI refuses a changedauthorPublicKeywithout it, and authors can't self-serve the label; - every admin who already installed the plugin sees
SIGNATURE_KEY_CHANGED(with both key fingerprints, to verify the new one with you out of band) and must re-trust the new key before their instance receives another update.
Without --allow-key-change, a differing key is still refused everywhere —
deliberately, because the accidental version of this (publishing from a second
machine whose freshly-generated key isn't the one you published under) wants the
original key restored, not a rotation.
Updating
Bump version in the manifest, commit, and run publish again with the new tag — it
detects the existing entry and prepends the new version, newest-first. By hand: re-pack,
cut a new vX.Y.Z release, then fold the new version onto your existing entry with
--merge:
npx trek-plugin-sdk entry --repo you/trek-plugin-flight-tracker --tag v1.1.0 \
--merge registry/plugins/flight-tracker.json \
--out registry/plugins/flight-tracker.json
--merge prepends the new version (keeping the array newest-first) and preserves
the rest of the entry. PR the updated file. Instances see the update on their next
registry poll; applying it is always an explicit admin action, and if a new
version requests more permissions the admin must re-approve — see
Plugin Permissions.