The docs told authors to run `npx create-trek-plugin` / `npx trek-plugin`, but nothing published under those names, so npx couldn't resolve them. - Ship one package, `trek-plugin-sdk`, with a bin that matches the package name (`trek-plugin-sdk`) so `npx trek-plugin-sdk <command>` resolves with zero install. The dispatcher gained a `create` subcommand, so every step (create/validate/pack/entry/release) runs through that one entry point. The short `trek-plugin` / `create-trek-plugin` bins still work once installed. - Package hardening for publish: repository+directory (monorepo subdir), homepage/bugs/author/engines, publishConfig public, a prepublishOnly that builds + tests, and a LICENSE file. - A publish workflow: pushing a `plugin-sdk-v*` tag builds and publishes with the NPM_TOKEN repo secret. - Docs (SDK README + the four wiki pages) now use `npx trek-plugin-sdk <cmd>`, the invocation that actually resolves.
7.1 KiB
Plugin Permissions
A plugin declares the permissions it needs in trek-plugin.json. You review that
list before you install — on the plugin's card under Discover — and it only
runs once you turn it on. Because plugins run in an isolated process, an
ungranted capability is physically unreachable, not just disallowed. See
Plugins for the isolation model.
Reference
| Permission | Grants | Notes |
|---|---|---|
db:own |
Read/write the plugin's own SQLite file via ctx.db — db.query, db.exec, and db.migrate |
A separate file per plugin — never trek.db. db.migrate runs a keyed, idempotent migration (schema/table creation, e.g. CREATE TABLE) once per id. ATTACH/DETACH/VACUUM/PRAGMA are refused. |
db:read:trips |
Read-only trip data via ctx.trips (getById, getPlaces, getReservations) |
Every call is membership-checked against the acting user — a plugin can't read a trip that user can't see. |
db:read:users |
Read-only public profile via ctx.users.getById |
Returns id, username, display name, avatar only — never password hashes, tokens, or secrets. |
ws:broadcast:trip |
Push a real-time event to a trip room via ctx.ws.broadcastToTrip |
Event types are force-namespaced plugin:<id>:<event> — a plugin can't forge a core event. |
ws:broadcast:user |
Push a real-time event to a user's connections | Same namespacing. |
hook:photo-provider |
Register as a photo provider in Memories | Implement the PhotoProvider interface. |
hook:calendar-source |
Register as a calendar source | Implement the CalendarSource interface. |
http:outbound / http:outbound:<host> |
Make outbound network requests | Requires a non-empty egress[]. Only a per-host http:outbound:<host> actually opens a host at runtime — see below. |
Outbound network — http:outbound vs http:outbound:<host>
This is the one permission with a subtlety worth reading twice.
Two independent guards restrict a plugin's network, and both are built from the
http:outbound:<host> permissions you grant — not from the egress[] array:
- the runtime egress guard inside the sandboxed child (any connect to a host that isn't allow-listed is rejected), and
- the plugin iframe's CSP
connect-src(the client can only fetch the same hosts).
egress[] is a separate declaration the manifest validator only checks for
presence, not contents. The rule it enforces is narrow:
- Only the permissions above are accepted; an unknown string fails validation.
- If any
http:outboundpermission (bare or per-host) is declared,egress[]must be non-empty. egress[]may not contain a bare*.
Because the validator never cross-checks egress[] against the granted hosts:
Warning
A host you list in
egress[]but forget to grant ashttp:outbound:<host>is silently blocked at runtime. Validation passes, install passes — then every request to that host is refused by the egress guard and the iframe CSP, with no manifest error to warn you. List every host you call as both anhttp:outbound:<host>permission and anegress[]entry, and keep the two identical.
Bare http:outbound (no host) satisfies the "non-empty egress[]" rule but
contributes no host to either guard — on its own it reaches nothing at
runtime. Use it only alongside the specific http:outbound:<host> grants for the
hosts you actually call.
A host may be an exact name (api.example.com) or a *.suffix wildcard
(*.example.com, matching the apex and any sub-domain). Even an allow-listed host
is refused if it resolves to a loopback / private / link-local / metadata address
(the SSRF backstop).
Declaring them
{
"permissions": ["db:own", "db:read:trips", "http:outbound:api.example.com"],
"egress": ["api.example.com"] // mirror every http:outbound:<host> here
}
Publishing — the trek-plugin CLI
The trek-plugin-sdk package ships a trek-plugin CLI that builds the release
artifact and the registry entry for you, so you never hand-compute a sha256,
size, or commit sha. Run it with npx trek-plugin-sdk <command>. The full submission
flow is in Publishing a Plugin.
| Command | What it does |
|---|---|
trek-plugin validate [dir] |
Manifest + layout checks: parses the manifest with the same rules as install, requires a README.md (warns if it has no screenshot or still holds template placeholders) and a built server/index.js, and warns if the directory name ≠ the plugin id. This is a subset of registry CI — CI additionally verifies the release tag/commit, the artifact's sha256, and the README over the network. A local pass predicts a CI pass. |
trek-plugin pack [dir] [--out plugin.zip] [--json] |
Validates, then builds plugin.zip in the installer's exact layout (trek-plugin.json, README.md, LICENSE, package.json at the root; server/ and client/ recursed) and prints its sha256 + byte size. Skips node_modules, .git, .ts and .map files, and refuses native binaries (.node, binding.gyp, prebuilds/) and over-size archives, same as the installer. docs/ is intentionally NOT shipped — the store fetches your screenshot from docs/screenshot.png in the repo. |
trek-plugin entry --repo <owner/name> --tag <vX.Y.Z> [--zip plugin.zip] [--merge entry.json] [--out file] |
Emits the ready-to-PR registry entry: commitSha (resolved from the tag), downloadUrl, sha256 + size (from the packed zip), and minTrekVersion (derived from the manifest's trek range, e.g. >=3.2.0 <4.0.0 → 3.2.0). --merge prepends this version onto an existing registry/plugins/<id>.json for an update, keeping versions newest-first. |
trek-plugin release [dir] --repo <owner/name> --tag <vX.Y.Z> |
The one-shot: pack → gh release create (uploads the zip) → print the registry entry. Requires the gh CLI authenticated. |
Registry policy
- No reserved namespaces. Any unique lowercase slug id is accepted (3–40
chars,
[a-z][a-z0-9-]*). The only refused ids areregistry,install, andrescan, which would collide with admin API routes. - Owner-binding still holds. An id is bound to its GitHub owner on first registration, so nobody can repoint an existing plugin id to a different repo.
- Optional author signing. A registry entry may carry an
authorPublicKey(stable across versions) and a per-versionsignature. TREK verifies it offline and pins the key trust-on-first-use. Signing is opt-in — an unsigned entry installs onsha256alone — but once a plugin has shipped signed, an unsigned update for it is refused. See Publishing a Plugin.
What is NOT covered
Isolation bounds what a plugin can touch, not its intent within a grant. A
plugin you allow to read trip data and reach api.example.com could send
that trip data there. So review the permissions and outbound hosts before you
install — grant only what you'd trust the plugin to do with your data. Prefer
Reviewed plugins and authors you trust. To build one, see Plugin Development.