mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
docs(plugins): rewrite the plugin wiki against the current code + tooling
The plugin wiki had drifted from the app and the SDK. Rewrote all four pages, verifying every command, permission, field, path and UI behaviour against source: - Plugins: activation is a toggle (no separate consent screen); install is the Discover tab (no "Browse plugins" button); you review permissions in the detail modal before installing; documents update + re-consent, the ⋯ menu, toolbar filters, capability chips and the health dot. - Plugin-Development: full manifest reference; ws:broadcast:trip/:user (there is no ws:broadcast:*); onLoad + onUnload; the trek:error bridge message and full context payload; trips.* only work in a route handler; asUserId is accepted-but-ignored; integration hooks are declared but not yet wired. - Plugin-Permissions: db:own also covers db.migrate; a host must appear as both an http:outbound:<host> permission and an egress[] entry or it's silently blocked; bare vs per-host outbound. - Plugin-Publishing: the new one-command flow (validate → pack → release → entry), size is a required entry field, signing reconciled with the schema, no reserved namespaces, and the --merge update path.
This commit is contained in:
+213
-35
@@ -1,7 +1,10 @@
|
||||
# Plugin Development
|
||||
|
||||
Build a plugin with the `trek-plugin-sdk` package. A plugin is a directory with a
|
||||
manifest, a built server entry, and (for page/widget plugins) a client bundle.
|
||||
manifest (`trek-plugin.json`), a built server entry, and — for page/widget
|
||||
plugins — a static client bundle. TREK runs your server code in an **isolated
|
||||
child process** and reaches it only over RPC; the browser part runs in a
|
||||
**sandboxed, opaque-origin iframe**. There is no other way in or out.
|
||||
|
||||
## Scaffold
|
||||
|
||||
@@ -17,97 +20,171 @@ my-plugin/
|
||||
trek-plugin.json # manifest
|
||||
server/index.js # your plugin code (built, plain JS)
|
||||
client/index.html # page/widget iframe (page/widget only)
|
||||
README.md # fill this in — the registry requires it
|
||||
README.md # fill this in — the registry requires a screenshot
|
||||
```
|
||||
|
||||
## The three plugin types
|
||||
|
||||
- **integration** — extends an existing feature (photo provider, calendar source,
|
||||
notification target). No UI of its own.
|
||||
- **integration** — background logic (jobs, routes) with no UI of its own. Photo-
|
||||
provider / calendar-source hook types exist in the SDK but are **not yet wired
|
||||
into the host** — see [Integration hooks](#integration-hooks-not-yet-functional).
|
||||
- **page** — adds a nav entry that opens a full-page sandboxed iframe.
|
||||
- **widget** — adds a card to the dashboard.
|
||||
- **widget** — adds a card to the dashboard (`sidebar` slot) or a hero-bar
|
||||
overlay (`hero` slot).
|
||||
|
||||
## The SDK package
|
||||
|
||||
`trek-plugin-sdk` is **injected at runtime** — the host makes
|
||||
`require('trek-plugin-sdk')` resolve inside the child, so **do not vendor it**
|
||||
into your artifact. Add it as a **devDependency** only, so you get types,
|
||||
`createMockHost` for tests, and the `trek-plugin` CLI:
|
||||
|
||||
```bash
|
||||
npm i -D trek-plugin-sdk
|
||||
```
|
||||
|
||||
## Writing the server
|
||||
|
||||
Your `server/index.js` exports a `definePlugin(...)` object. Everything reaches
|
||||
TREK through the `ctx` argument — there is no other way out of the sandbox.
|
||||
TREK through the `ctx` argument.
|
||||
|
||||
```js
|
||||
const { definePlugin } = require('trek-plugin-sdk')
|
||||
|
||||
module.exports = definePlugin({
|
||||
// Runs once when the plugin is activated.
|
||||
// Runs once when the plugin is activated. NOTE: onLoad has no user context —
|
||||
// ctx.trips.* is refused here (see the ctx table).
|
||||
async onLoad(ctx) {
|
||||
await ctx.db.migrate('001_init', 'CREATE TABLE cache (k TEXT PRIMARY KEY, v TEXT)')
|
||||
await ctx.db.migrate('001_init', 'CREATE TABLE IF NOT EXISTS cache (k TEXT PRIMARY KEY, v TEXT)')
|
||||
ctx.log.info('loaded')
|
||||
},
|
||||
|
||||
// Runs once on deactivation/stop. Use it to flush or release resources.
|
||||
async onUnload(ctx) {
|
||||
ctx.log.info('unloading')
|
||||
},
|
||||
|
||||
// HTTP routes, mounted at /api/plugins/<id><path>.
|
||||
routes: [
|
||||
{ method: 'GET', path: '/status', auth: true, async handler(req, ctx) {
|
||||
const rows = await ctx.db.query('SELECT COUNT(*) AS n FROM cache')
|
||||
return { status: 200, headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ n: rows[0].n, user: req.user?.username }) }
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ n: rows[0].n, user: req.user?.username }),
|
||||
}
|
||||
}},
|
||||
],
|
||||
|
||||
// Scheduled jobs — TREK owns the cron and calls your handler.
|
||||
// Scheduled jobs — TREK owns the cron and calls your handler (no user context).
|
||||
jobs: [
|
||||
{ id: 'refresh', schedule: '*/15 * * * *', async handler(ctx) { /* … */ } },
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
The routes and job ids you declare here are the **authoritative** ones: the host
|
||||
reads them off your loaded definition (a route's array index is its internal id).
|
||||
The `routes` block the scaffold writes into `trek-plugin.json` is only a
|
||||
declaration for readers — the manifest parser does not consume it.
|
||||
|
||||
### The `ctx` object
|
||||
|
||||
| Area | Methods | Requires |
|
||||
|---|---|---|
|
||||
| `ctx.db` | `query` / `exec` / `migrate` against your **own** SQLite file | `db:own` |
|
||||
| `ctx.db` | `query(sql, …args)` / `exec(sql, …args)` / `migrate(id, sql)` against your **own** SQLite file | `db:own` |
|
||||
| `ctx.trips` | `getById` / `getPlaces` / `getReservations` (membership-checked) | `db:read:trips` |
|
||||
| `ctx.users` | `getById` (public profile only — never secrets) | `db:read:users` |
|
||||
| `ctx.ws` | `broadcastToTrip` / `broadcastToUser` (events namespaced `plugin:<id>:…`) | `ws:broadcast:*` |
|
||||
| `ctx.config` | your instance settings (secrets delivered decrypted) | — |
|
||||
| `ctx.users` | `getById(id)` — public profile only (`id, username, display_name, avatar`) | `db:read:users` |
|
||||
| `ctx.ws.broadcastToTrip(tripId, event, data)` | broadcast to a trip's members (event forced to `plugin:<id>:<event>`) | `ws:broadcast:trip` |
|
||||
| `ctx.ws.broadcastToUser(userId, event, data)` | broadcast to one user | `ws:broadcast:user` |
|
||||
| `ctx.config` | your resolved settings (secrets delivered decrypted) | — |
|
||||
| `ctx.log` | `info` / `warn` / `error` → your error log | — |
|
||||
| `ctx.id` | your plugin id (string) | — |
|
||||
|
||||
Calling a capability your manifest didn't declare throws `PERMISSION_DENIED`.
|
||||
Calling a method your manifest didn't grant returns `PERMISSION_DENIED`; a method
|
||||
the host doesn't expose at all returns `UNKNOWN_METHOD`.
|
||||
|
||||
**`ctx.trips` only works inside a route handler.** The host binds the acting user
|
||||
from the authenticated request and membership-checks every trip read against it.
|
||||
`onLoad` and `jobs` have **no user**, so their trip reads are refused with
|
||||
`RESOURCE_FORBIDDEN`. The SDK's `getById(tripId, asUserId?)` signature keeps an
|
||||
`asUserId` parameter for source compatibility, but **the host ignores it** — you
|
||||
cannot read another user's trips by passing an id.
|
||||
|
||||
### Route auth
|
||||
|
||||
Routes are authenticated by default (`req.user` is the logged-in user). Set
|
||||
`auth: false` for OAuth callbacks or webhooks that can't carry a session.
|
||||
`auth: false` for OAuth callbacks or webhooks that can't carry a session. The
|
||||
proxy forwards only `{ method, path, query, body, user }` — your code never sees
|
||||
raw headers or the session cookie.
|
||||
|
||||
## Writing the client (page / widget)
|
||||
|
||||
The iframe is sandboxed at an **opaque origin** — it can't read cookies or the
|
||||
parent page. It talks to TREK only via `postMessage`:
|
||||
The iframe is served same-origin from `/plugin-frame/<id>/…` but sandboxed
|
||||
**without `allow-same-origin`**, so it runs at an **opaque origin**: it can't read
|
||||
cookies or the parent DOM. It talks to TREK only via `postMessage` (target origin
|
||||
must be `'*'` — an opaque frame has no nameable origin).
|
||||
|
||||
```js
|
||||
// Announce readiness — TREK replies with trek:context.
|
||||
window.parent.postMessage({ type: 'trek:ready' }, '*')
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const m = e.data
|
||||
if (m.type === 'trek:context') { /* m.theme, m.locale, m.tripId, m.userId */ }
|
||||
if (m.type === 'trek:response' && m.requestId === '1') { /* … */ }
|
||||
if (m.type === 'trek:context') {
|
||||
// m.tripId, m.userId (string|null), m.theme ('light'|'dark'), m.locale, m.hostOrigin
|
||||
}
|
||||
if (m.type === 'trek:response' && m.requestId === '1') { /* m.data */ }
|
||||
if (m.type === 'trek:error' && m.requestId === '1') { /* m.code, m.message */ }
|
||||
})
|
||||
|
||||
// Call one of your own server routes (TREK proxies it with the user's session):
|
||||
// Call one of your OWN server routes — TREK proxies it with the user's session:
|
||||
window.parent.postMessage({ type: 'trek:invoke', requestId: '1', sub: '/status', method: 'GET' }, '*')
|
||||
```
|
||||
|
||||
Bridge messages you can send: `trek:ready`, `trek:context:request`,
|
||||
`trek:navigate {to}`, `trek:notify {level,message}`, `trek:resize {height}`,
|
||||
`trek:invoke {requestId,sub,method,body}`.
|
||||
**Messages you send to TREK (inbound bridge):**
|
||||
|
||||
| Message | Payload | Effect |
|
||||
|---|---|---|
|
||||
| `trek:ready` | — | TREK replies with `trek:context` |
|
||||
| `trek:context:request` | — | re-request the context |
|
||||
| `trek:navigate` | `{ to }` | in-app navigation (relative paths only) |
|
||||
| `trek:notify` | `{ level, message }` | toast; `level` = `info`/`success`/`warning`/`error` |
|
||||
| `trek:resize` | `{ height }` | set the iframe height (capped at 2000px) |
|
||||
| `trek:invoke` | `{ requestId, sub, method, body }` | call your own route; resolves as `trek:response` or `trek:error` |
|
||||
|
||||
**Messages TREK sends you (host bridge):**
|
||||
|
||||
| Message | Payload |
|
||||
|---|---|
|
||||
| `trek:context` | `{ tripId, userId, theme, locale, hostOrigin }` — `userId` is a **string** or `null` |
|
||||
| `trek:response` | `{ requestId, data }` — a successful `trek:invoke` |
|
||||
| `trek:error` | `{ requestId, code, message }` — a failed `trek:invoke` (`code` is the HTTP status or `"error"`) |
|
||||
|
||||
The frame's CSP is locked down per plugin: `default-src 'none'`, own scripts/styles
|
||||
only, `connect-src` limited to your declared `egress[]` hosts, no popups.
|
||||
|
||||
## Settings
|
||||
|
||||
Declare settings in the manifest; TREK renders the form (you write no settings
|
||||
UI). `scope: "instance"` settings are set once by the admin; `scope: "user"`
|
||||
settings are per-user. `secret: true` fields are stored encrypted and delivered
|
||||
to your plugin decrypted through `ctx.config` (instance) — never to the iframe.
|
||||
decrypted through `ctx.config` (server-side only) — never to the iframe. Resolved
|
||||
values arrive in `ctx.config`.
|
||||
|
||||
## Integration hooks (not yet functional)
|
||||
|
||||
The SDK exports `PhotoProvider` / `CalendarSource` interfaces and a
|
||||
`hooks: { photoProvider, calendarSource }` field on the plugin definition, and the
|
||||
`hook:photo-provider` / `hook:calendar-source` permissions validate. **However the
|
||||
host runtime does not consume `hooks` yet** — it only invokes `onLoad`, `onUnload`,
|
||||
`routes` and `jobs`. Treat these as a reserved surface: you can declare them, but
|
||||
TREK will not call them today. Build integrations with routes + jobs for now.
|
||||
|
||||
## Testing without a running TREK
|
||||
|
||||
`createMockHost` gives you a `ctx` that enforces the **same** permission model:
|
||||
`createMockHost` gives you a `ctx` that enforces the **same** permission model, so
|
||||
a test can prove your plugin degrades gracefully when a grant is missing:
|
||||
|
||||
```js
|
||||
import { createMockHost } from 'trek-plugin-sdk/testing'
|
||||
@@ -116,21 +193,122 @@ const { ctx, broadcasts } = createMockHost({
|
||||
grants: ['db:read:trips'],
|
||||
trips: { 1: { members: [42], data: { id: 1, name: 'Japan' } } },
|
||||
})
|
||||
await plugin.onLoad(ctx) // run your load logic
|
||||
await expect(ctx.trips.getById(1, 99)).rejects… // proves your access checks
|
||||
await ctx.trips.getById(1, 42) // ok — member
|
||||
await expect(ctx.trips.getById(1, 99)).rejects… // RESOURCE_FORBIDDEN
|
||||
await expect(ctx.db.query('SELECT 1')).rejects… // PERMISSION_DENIED (no db:own)
|
||||
```
|
||||
|
||||
The mock db is a recorder — set `queryResults` for canned rows, or use an
|
||||
integration test for real SQL.
|
||||
|
||||
## Rules
|
||||
|
||||
- **No native modules** (`.node`, `binding.gyp`, prebuilds) — they're rejected.
|
||||
- **Vendor your dependencies** — TREK never runs `npm install` on a plugin.
|
||||
- **No native modules** (`.node`, `binding.gyp`, `prebuilds/`) — rejected at pack
|
||||
and install time.
|
||||
- **Don't vendor `trek-plugin-sdk`** — it's injected at runtime (devDependency
|
||||
only). Vendor any *other* runtime deps: TREK never runs `npm install` on a plugin.
|
||||
- **Ship built JS** in `server/index.js` and pre-built static files in `client/`.
|
||||
- Declare every outbound host in `egress[]` when you use `http:outbound`.
|
||||
`.ts` and `.map` files are stripped by `pack`.
|
||||
- Declare every outbound host in `egress[]` whenever you use `http:outbound`.
|
||||
|
||||
## Validate before publishing
|
||||
## Manifest reference (`trek-plugin.json`)
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | string, **required** | lowercase slug, `^[a-z][a-z0-9-]{2,39}$` (3–40 chars). Must match the directory name. |
|
||||
| `name` | string, **required** | display name; also the page nav label. |
|
||||
| `version` | string, **required** | semver (`1.2.3`, optional pre-release). |
|
||||
| `apiVersion` | number | plugin API version (currently `1`; `PLUGIN_API_VERSION`). Defaults to `1`. |
|
||||
| `type` | string, **required** | `integration` \| `page` \| `widget`. |
|
||||
| `trek` | string | supported TREK range, e.g. `">=3.2.0 <4.0.0"`. Its lower bound becomes `minTrekVersion` in the registry entry. |
|
||||
| `author` | string | shown in the store. |
|
||||
| `description` | string | one-line summary for the store. |
|
||||
| `icon` | string | lucide-react icon name (default `Blocks`); used for the page nav entry. |
|
||||
| `homepage` | string | project URL. |
|
||||
| `license` | string | shown in the store detail (read from the manifest, not enforced). |
|
||||
| `nativeModules` | boolean | must be `false`/absent — `true` is rejected. |
|
||||
| `permissions` | string[] | see below. |
|
||||
| `egress` | string[] | allowed outbound hosts; required (non-empty, no bare `*`) when any `http:outbound` permission is present. |
|
||||
| `capabilities.widget` | object | `{ title, slot, defaultSize }` — `slot` is `sidebar` (default) or `hero`. |
|
||||
| `settings` | array | setting fields (below). |
|
||||
|
||||
**Permissions** (unknown values are rejected):
|
||||
|
||||
| Permission | Grants |
|
||||
|---|---|
|
||||
| `db:own` | `ctx.db` — your own SQLite file |
|
||||
| `db:read:trips` | `ctx.trips.*` (membership-checked, route handlers only) |
|
||||
| `db:read:users` | `ctx.users.getById` |
|
||||
| `ws:broadcast:trip` | `ctx.ws.broadcastToTrip` |
|
||||
| `ws:broadcast:user` | `ctx.ws.broadcastToUser` |
|
||||
| `http:outbound` or `http:outbound:<host>` | outbound HTTP to `egress[]` hosts |
|
||||
| `hook:photo-provider` / `hook:calendar-source` | reserved (see [Integration hooks](#integration-hooks-not-yet-functional)) |
|
||||
|
||||
> There is **no `ws:broadcast:*`** — use `ws:broadcast:trip` and/or
|
||||
> `ws:broadcast:user` explicitly.
|
||||
|
||||
**Settings field** (`settings[]`):
|
||||
|
||||
| Key | Notes |
|
||||
|---|---|
|
||||
| `key` | **required** identifier; empty-key entries are dropped. |
|
||||
| `label` | form label. |
|
||||
| `input_type` | **snake_case**; e.g. `text` (default), `password`, `number`, `select`. |
|
||||
| `scope` | `instance` (default) or `user`. |
|
||||
| `required` | boolean. |
|
||||
| `secret` | boolean — encrypted at rest, decrypted only into `ctx.config`. |
|
||||
| `placeholder`, `hint` | form hints. |
|
||||
| `options` | `[{ value, label }]` for select inputs. |
|
||||
| `oauth` | `{ initPath, callbackPath }` for OAuth flows. |
|
||||
|
||||
**Page nav:** the host builds a page plugin's nav entry from the top-level `name`
|
||||
and `icon`. `create-trek-plugin` also scaffolds a `capabilities.nav` block, but the
|
||||
installed-manifest parser only consumes `capabilities.widget` — set `name`/`icon`
|
||||
to control the nav entry.
|
||||
|
||||
See [[Plugin Permissions|Plugin-Permissions]] for the full permission model.
|
||||
|
||||
## The `trek-plugin` CLI
|
||||
|
||||
Author commands (from `trek-plugin-sdk`):
|
||||
|
||||
```bash
|
||||
npx trek-plugin validate .
|
||||
# 1. Manifest + layout checks (a subset of the registry CI — CI additionally
|
||||
# verifies the GitHub release exists, the artifact sha256, and the README
|
||||
# over the network).
|
||||
trek-plugin validate [dir]
|
||||
|
||||
# 2. Build plugin.zip in the installer's exact layout. Prints sha256 + byte size,
|
||||
# refuses native binaries, enforces the same size limits (25MB/file, 50MB total).
|
||||
# Ships trek-plugin.json, README.md, LICENSE(.md), package.json + server/ + client/.
|
||||
# docs/ is intentionally NOT shipped — the store fetches docs/screenshot.png
|
||||
# from your repo. --json prints a machine-readable result.
|
||||
trek-plugin pack [dir] [--out plugin.zip] [--json]
|
||||
|
||||
# 3. Emit the ready-to-PR registry entry: commitSha (resolved from the git tag),
|
||||
# downloadUrl, sha256, size and minTrekVersion (derived from the manifest
|
||||
# 'trek' range) all computed for you. --merge prepends a new version onto an
|
||||
# existing entry (the update case, kept newest-first).
|
||||
trek-plugin entry --repo owner/name --tag vX.Y.Z [--zip plugin.zip] [--merge entry.json] [--out file]
|
||||
|
||||
# 4. One shot: pack -> create the GitHub release (via gh) -> print the entry.
|
||||
trek-plugin release [dir] --repo owner/name --tag vX.Y.Z
|
||||
```
|
||||
|
||||
Runs the same checks the registry CI runs. See [[Publishing a Plugin|Plugin-Publishing]].
|
||||
To publish, open a PR that adds the emitted JSON as
|
||||
`registry/plugins/<id>.json` in the TREK-Plugins registry.
|
||||
|
||||
## Registry & publishing
|
||||
|
||||
- **No reserved namespaces** — any unique slug id is accepted. (A tiny set of ids
|
||||
like `registry`/`install`/`rescan` is blocked only because they'd collide with
|
||||
admin API routes.)
|
||||
- **Owner-binding** still prevents anyone but the original author from repointing
|
||||
an existing id to a different repo.
|
||||
- **Optional author signing:** an entry may carry `authorPublicKey` (stable,
|
||||
TOFU-pinned on first install) and each version a `signature` over the artifact
|
||||
bytes. Unsigned plugins install on sha256 alone; a plugin that was signed can't
|
||||
later go unsigned or swap its key without an explicit admin re-trust.
|
||||
|
||||
Full walkthrough: [[Publishing a Plugin|Plugin-Publishing]]. Overview:
|
||||
[[Plugins|Plugins]].
|
||||
|
||||
+76
-14
@@ -1,42 +1,104 @@
|
||||
# Plugin Permissions
|
||||
|
||||
A plugin declares the permissions it needs in `trek-plugin.json`. At activation
|
||||
the admin sees the list and approves it — and because plugins run in an isolated
|
||||
process, **an unapproved capability is physically unreachable**, not just
|
||||
disallowed.
|
||||
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` | A separate file per plugin — never `trek.db`. `ATTACH`/`PRAGMA` are refused. |
|
||||
| `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[]` list of hosts. The plugin's client CSP and the runtime egress guard are limited to those hosts. |
|
||||
| `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:outbound` permission (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 as `http: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* an
|
||||
> `http:outbound:<host>` permission *and* an `egress[]` 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
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"permissions": ["db:own", "db:read:trips", "http:outbound:api.example.com"],
|
||||
"egress": ["api.example.com"] // required whenever http:outbound is declared
|
||||
"egress": ["api.example.com"] // mirror every http:outbound:<host> here
|
||||
}
|
||||
```
|
||||
|
||||
Rules the manifest validator enforces:
|
||||
## Publishing — the `trek-plugin` CLI
|
||||
|
||||
- Only the permissions above are accepted; an unknown string fails validation.
|
||||
- `http:outbound` (with or without a host) requires a non-empty `egress[]`.
|
||||
- `egress[]` may not contain a bare `*`.
|
||||
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 <command>`. The full submission
|
||||
flow is in [[Publishing a Plugin|Plugin-Publishing]].
|
||||
|
||||
| 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 are `registry`, `install`, and
|
||||
`rescan`, 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-version `signature`. TREK verifies it offline
|
||||
and pins the key trust-on-first-use. Signing is opt-in — an unsigned entry
|
||||
installs on `sha256` alone — but once a plugin has shipped signed, an unsigned
|
||||
update for it is refused. See [[Publishing a Plugin|Plugin-Publishing]].
|
||||
|
||||
## 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. That is the point of the consent screen — grant only what
|
||||
you'd trust the plugin to do with your data. Prefer **Reviewed** plugins and
|
||||
authors you trust.
|
||||
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|Plugin-Development]].
|
||||
|
||||
+162
-71
@@ -2,75 +2,154 @@
|
||||
|
||||
Plugins are distributed from a static registry — the
|
||||
[TREK-Plugins](https://github.com/mauriceboe/TREK-Plugins) GitHub repo. There is
|
||||
no upload server and no account: you host your plugin in your own GitHub repo and
|
||||
list it with a pull request.
|
||||
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.
|
||||
|
||||
## 1. Host your plugin
|
||||
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.
|
||||
|
||||
Put your plugin in a **public GitHub repo** (convention: `trek-plugin-<id>`). It
|
||||
must contain a `trek-plugin.json` at the root, a built `server/index.js`, a
|
||||
filled-in `README.md`, and (page/widget) a built `client/`.
|
||||
## The short version
|
||||
|
||||
## 2. Tag a release
|
||||
```bash
|
||||
npx trek-plugin validate . # manifest + layout sanity
|
||||
npx trek-plugin pack . # build plugin.zip, print sha256 + size
|
||||
gh release create v1.0.0 plugin.zip --repo you/trek-plugin-flight-tracker
|
||||
npx trek-plugin entry --repo you/trek-plugin-flight-tracker --tag v1.0.0 \
|
||||
--out registry/plugins/flight-tracker.json # the ready-to-PR entry
|
||||
```
|
||||
|
||||
Tag `vX.Y.Z` where `X.Y.Z` **equals** `version` in your manifest. Either attach a
|
||||
`plugin.zip` release asset, or rely on the GitHub source archive at that commit.
|
||||
…then open a PR to **TREK-Plugins** adding that one
|
||||
`registry/plugins/<id>.json` file. `trek-plugin release` collapses the middle
|
||||
three steps into one command (see below).
|
||||
|
||||
## 3. Validate locally
|
||||
## 1. Host and build your plugin
|
||||
|
||||
Put your plugin in a **public GitHub repo** (convention: `trek-plugin-<id>`).
|
||||
`create-trek-plugin` scaffolds the layout:
|
||||
|
||||
```bash
|
||||
npx create-trek-plugin flight-tracker --type widget # integration | page | widget
|
||||
```
|
||||
|
||||
A publishable plugin has, at the repo root:
|
||||
|
||||
- `trek-plugin.json` — the manifest (see [[Plugin Development|Plugin-Development]])
|
||||
- `server/index.js` — the built server entry (required)
|
||||
- `client/` — the built frontend (only for `page`/`widget` plugins)
|
||||
- `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
|
||||
|
||||
## 2. Validate
|
||||
|
||||
```bash
|
||||
npx trek-plugin validate .
|
||||
```
|
||||
|
||||
This runs the exact checks the registry CI runs — a local pass predicts a CI
|
||||
pass.
|
||||
`validate` runs the manifest checks plus a light layout/README sanity pass:
|
||||
it fails if `trek-plugin.json` is invalid or `server/index.js` is missing, and
|
||||
warns if the directory name doesn't match the plugin `id`, the README has no
|
||||
screenshot, or the README still contains scaffold placeholders. This is a
|
||||
**subset** of what CI runs — CI additionally verifies the release, the artifact's
|
||||
SHA-256, and the README over the network — so a clean local run predicts a clean
|
||||
CI run but doesn't replace it.
|
||||
|
||||
## 4. Fork the registry and open a PR
|
||||
## 3. Pack
|
||||
|
||||
You don't have write access to the registry, so **fork
|
||||
[TREK-Plugins](https://github.com/mauriceboe/TREK-Plugins)** to your own account,
|
||||
add your entry there, and open a pull request from your fork back to the
|
||||
registry's `main` branch.
|
||||
|
||||
Add one file, `registry/plugins/<id>.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"id": "flight-tracker",
|
||||
"name": "Flight Tracker",
|
||||
"author": "Your Name",
|
||||
"description": "Live flight status widget.",
|
||||
"repo": "you/trek-plugin-flight-tracker",
|
||||
"type": "widget",
|
||||
"versions": [{
|
||||
"version": "1.0.0",
|
||||
"gitTag": "v1.0.0",
|
||||
"commitSha": "<40-hex commit the tag points at>",
|
||||
"downloadUrl": "https://github.com/you/trek-plugin-flight-tracker/releases/download/v1.0.0/plugin.zip",
|
||||
"sha256": "<sha256 of that artifact>",
|
||||
"minTrekVersion": "3.2.0",
|
||||
"apiVersion": 1,
|
||||
"nativeModules": false
|
||||
}]
|
||||
}
|
||||
```bash
|
||||
npx trek-plugin pack . # writes ./plugin.zip
|
||||
npx trek-plugin pack . --out dist.zip # custom output path
|
||||
npx trek-plugin pack . --json # machine-readable result
|
||||
```
|
||||
|
||||
CI validates the entry and the artifact, and the maintainer reviews the source at
|
||||
the pinned commit before merging.
|
||||
`pack` validates first, then builds `plugin.zip` in the installer's exact layout
|
||||
and prints the **sha256** and **size** you'd otherwise compute by hand. 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 your
|
||||
> `docs/screenshot.png` straight from the repo at the pinned commit, so keep it
|
||||
> committed to GitHub but out of the zip — `pack` handles this for you.
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
npx trek-plugin 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 `minTrekVersion` (the lower bound of the
|
||||
manifest's `trek` range, e.g. `">=3.2.0 <4.0.0"` → `3.2.0`). 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`
|
||||
|
||||
```bash
|
||||
npx trek-plugin 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. Open a registry PR
|
||||
|
||||
You don't have write access, so **fork
|
||||
[TREK-Plugins](https://github.com/mauriceboe/TREK-Plugins)**, 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`](https://github.com/mauriceboe/TREK-Plugins/blob/main/schema/plugin-entry.schema.json);
|
||||
[`schema/example-entry.json`](https://github.com/mauriceboe/TREK-Plugins/blob/main/schema/example-entry.json)
|
||||
is the canonical shape. `size` is **required** (a common omission), as are
|
||||
`commitSha`, `downloadUrl`, `sha256`, `minTrekVersion`, `apiVersion`, and
|
||||
`nativeModules: false` on every version — all of which `trek-plugin entry` fills
|
||||
in for you.
|
||||
|
||||
## What CI enforces
|
||||
|
||||
**Entry:** valid schema · `id` matches the filename and is a valid slug ·
|
||||
reserved namespaces (`trek-`, `official-`) blocked · your `id` is bound to your
|
||||
GitHub owner on first registration (nobody can repoint it later) · homoglyph
|
||||
check · the release tag exists and points at the pinned commit · manifest parity
|
||||
· **the artifact's SHA-256 matches the pin** · **no native binaries** ·
|
||||
`egress[]` present when `http:outbound` is declared.
|
||||
CI runs `scripts/validate-entry.mjs` and `scripts/check-readme.mjs` on every
|
||||
changed `registry/plugins/*.json`.
|
||||
|
||||
**README:** must exist, have the required sections (*What it does / Screenshots /
|
||||
Permissions / Setup*), contain **at least one screenshot that actually resolves**,
|
||||
have real content (not the unfilled template), and explain every declared
|
||||
permission.
|
||||
**Entry** (`validate-entry.mjs`): valid schema · `id` matches the filename and
|
||||
the slug pattern `^[a-z][a-z0-9-]{2,39}$` · 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 · the release tag exists
|
||||
and resolves to `commitSha` · manifest parity at that commit (`id`, `version`,
|
||||
`type`, `apiVersion`, and `nativeModules` must not be `true`) · **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. Any unique slug is fine — **there are no reserved
|
||||
namespaces**.
|
||||
|
||||
**README** (`check-readme.mjs`, fetched at the pinned commit): must exist at the
|
||||
repo root, contain the sections **What it does / Screenshots / Permissions /
|
||||
Setup**, carry **at least one screenshot that resolves to a real image**
|
||||
(a relative `docs/screenshot.png` is resolved against the commit), 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
|
||||
|
||||
@@ -79,34 +158,36 @@ permission.
|
||||
|
||||
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.
|
||||
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 cannot ship attacker code under your name. TREK verifies the signature
|
||||
offline (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.
|
||||
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.
|
||||
|
||||
**One-time — create a key:**
|
||||
|
||||
```
|
||||
minisign -G # writes minisign.key (keep secret) + minisign.pub
|
||||
```bash
|
||||
minisign -G # writes minisign.key (keep secret) + minisign.pub
|
||||
```
|
||||
|
||||
Put the public key in your registry entry as `authorPublicKey` (the base64 payload
|
||||
line from `minisign.pub`). It is stable across versions.
|
||||
Put the base64 payload line from `minisign.pub` in your entry as
|
||||
`authorPublicKey` (stable across versions).
|
||||
|
||||
**Each release — sign the artifact:**
|
||||
|
||||
```
|
||||
minisign -Sm plugin.zip # writes plugin.zip.minisig
|
||||
```bash
|
||||
minisign -Sm plugin.zip # writes plugin.zip.minisig
|
||||
```
|
||||
|
||||
Add the base64 signature line from `plugin.zip.minisig` to that version as
|
||||
`signature`, next to its `sha256`. Example entry:
|
||||
Add the base64 line from `plugin.zip.minisig` to that version as `signature`,
|
||||
next to its `sha256`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -121,12 +202,22 @@ Add the base64 signature line from `plugin.zip.minisig` to that version as
|
||||
```
|
||||
|
||||
Signing is **opt-in**: an entry without `authorPublicKey`/`signature` installs on
|
||||
`sha256` alone, exactly as before. But once a plugin has shipped signed, an
|
||||
*unsigned* update for it is refused — don't drop the signature between versions.
|
||||
`sha256` alone. But once a plugin has shipped signed, an *unsigned* update is
|
||||
refused — don't drop the signature between versions.
|
||||
|
||||
## Updating
|
||||
|
||||
Add a new version entry (new `version`/`gitTag`/`commitSha`/`sha256`) to your
|
||||
plugin file and PR it. Instances see the update on their next registry poll;
|
||||
updating is always an explicit admin action, and if a new version requests **more**
|
||||
permissions the admin must re-approve.
|
||||
Bump `version` in the manifest, re-`pack`, cut a new `vX.Y.Z` release, then fold
|
||||
the new version onto your existing entry with `--merge`:
|
||||
|
||||
```bash
|
||||
npx trek-plugin 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|Plugin-Permissions]].
|
||||
|
||||
+111
-29
@@ -12,67 +12,149 @@ public registry, and installed at the instance owner's discretion.
|
||||
> only the access you'd trust it to have with your data, and prefer plugins
|
||||
> marked **Reviewed**.
|
||||
|
||||
## Plugin types
|
||||
|
||||
A plugin declares one `type` in its manifest, which decides where it surfaces:
|
||||
|
||||
| Type | Where it shows | Typical use |
|
||||
|---|---|---|
|
||||
| **widget** | On the dashboard — in the sidebar, or (with `slot: "hero"`) as a boarding-pass style hero overlay | At-a-glance info like flight status or weather |
|
||||
| **page** | As its own entry in the top navigation | A full self-contained tool |
|
||||
| **integration** | Nowhere visible — registers into TREK via hooks (e.g. a photo provider or calendar source) | Feed data into existing TREK features |
|
||||
|
||||
## Enabling plugins
|
||||
|
||||
The plugin runtime is **off by default**. To turn it on, set the environment
|
||||
variable on your instance and restart:
|
||||
variable to exactly `true` and restart:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- TREK_PLUGINS_ENABLED=true
|
||||
```
|
||||
|
||||
When it's off, the **Admin → Plugins** tab shows a "disabled by server
|
||||
configuration" banner and nothing runs.
|
||||
When it's off, the **Admin → Plugins** tab shows a "turned off
|
||||
(TREK_PLUGINS_ENABLED)" banner and nothing runs. Setting it back to `false` is a
|
||||
kill switch — installed plugins stay on disk, deactivated and harmless.
|
||||
|
||||
## The isolation model — what a plugin can and can't do
|
||||
|
||||
Each active plugin runs as a **separate OS process** with a deliberately empty
|
||||
environment:
|
||||
Each active plugin runs as a **separate OS process**, started under Node's
|
||||
permission model (`--permission`) with filesystem reads scoped to just its own
|
||||
code:
|
||||
|
||||
- It has **no** access to `JWT_SECRET`, the database connection, or any TREK
|
||||
secret — those are simply not in its process.
|
||||
- It **cannot** open `trek.db`. Its own data lives in a separate SQLite file it
|
||||
secret — those are simply not reachable by its process.
|
||||
- It **cannot** open `trek.db`, write files, spawn child processes, use worker
|
||||
threads, or load native addons. Its own data lives in a separate SQLite file it
|
||||
reaches only through TREK.
|
||||
- It talks to TREK exclusively over an internal RPC channel, and TREK only
|
||||
answers the capabilities the plugin's manifest **declares and you approve**.
|
||||
An ungranted call is refused, not merely ignored.
|
||||
- A page/widget's interface runs in a **sealed browser frame** that can't read
|
||||
your session cookie or touch the surrounding TREK page.
|
||||
- If a plugin crashes, hangs, or runs out of memory, only *its* process dies —
|
||||
TREK keeps running and restarts or disables it.
|
||||
TREK keeps running and can restart or disable it.
|
||||
|
||||
This means the permission list you approve at activation is a **real boundary**,
|
||||
not a label. See [[Plugin Permissions|Plugin-Permissions]] for exactly what each
|
||||
permission grants.
|
||||
This means the permission list you approve is a **real boundary**, not a label.
|
||||
It bounds *what* a plugin can touch, not its intent within a grant: a plugin you
|
||||
let read trips **and** reach a host could send those trips there. See
|
||||
[[Plugin Permissions|Plugin-Permissions]] for exactly what each permission grants.
|
||||
|
||||
## The Admin → Plugins panel
|
||||
|
||||
A single panel with a segmented **Installed / Discover** switch at the top left,
|
||||
plus a toolbar:
|
||||
|
||||
- **Search** — filters the current list by name/description (and author, in Discover).
|
||||
- **Type** filter — All / Widget / Integration / Page.
|
||||
- **Status** filter (Installed view only) — All / Active / Off / Update available / Error.
|
||||
- **Sort** — Name / Recently updated / Updates first.
|
||||
- **Rescan** — re-reads the on-disk plugins directory (see [Installing](#installing-a-plugin)).
|
||||
|
||||
Each installed row shows an icon tile with a **health dot** (green = active,
|
||||
blue pulse = starting, red = error, amber = disabled/incompatible, faint = inactive),
|
||||
the name and version, a **Reviewed** shield if applicable, and **capability
|
||||
chips** derived from its declared permissions — "Reads your trips", "Dashboard
|
||||
widget", "Real-time updates", "Provides photos", outbound hosts, and so on — so a
|
||||
plugin's real reach is legible without opening anything.
|
||||
|
||||
## Reviewing a plugin before install
|
||||
|
||||
In **Discover**, click any card to open its **detail modal**. This is where you
|
||||
review a plugin *before* it touches your instance. It fetches the plugin's live
|
||||
manifest (at the reviewed commit) and lays out:
|
||||
|
||||
- **What it can access** — the permission-derived capabilities in plain language.
|
||||
- **Connects to** — the outbound hosts it declared (`egress`).
|
||||
- **Setup** — configuration fields it will ask for, with scope (instance/user) and
|
||||
whether each is required.
|
||||
- **Details** — version, download size, minimum TREK version, review date, plus
|
||||
links to the source repo and homepage.
|
||||
|
||||
A **Reviewed** badge means a TREK maintainer scanned that exact version's source
|
||||
for malware — **not** that it works well or is harmless. It is not an ongoing
|
||||
guarantee. Read the access list and outbound hosts, not just the description.
|
||||
|
||||
## Installing a plugin
|
||||
|
||||
Two ways, both from **Admin → Plugins**:
|
||||
|
||||
1. **From the registry.** Click *Browse plugins*, pick one, and *Install*. TREK
|
||||
downloads the pinned version from GitHub, verifies its SHA-256 against the
|
||||
registry, safely unpacks it, and registers it — **inactive**. Nothing runs
|
||||
yet.
|
||||
2. **From disk.** Drop a plugin directory into the `/plugins` volume and click
|
||||
*Rescan* (or restart). TREK discovers it and registers it inactive.
|
||||
1. **From the registry.** In **Discover**, open a plugin and click **Install**
|
||||
(also available directly on the card). TREK downloads the pinned version, verifies
|
||||
its SHA-256 against the registry (and an author signature if the plugin ships one),
|
||||
safely unpacks it, re-validates the manifest, and registers it — **inactive**.
|
||||
Nothing runs yet.
|
||||
2. **From disk.** Drop a plugin directory into the plugins code directory
|
||||
(`server/data/plugins`, or your `TREK_PLUGINS_DIR` volume) and click **Rescan**
|
||||
(or restart). TREK discovers it and registers it inactive.
|
||||
|
||||
## Activating a plugin
|
||||
|
||||
Activation is a separate, deliberate step. You review the plugin's requested
|
||||
permissions and outbound hosts on a consent screen, then click **Activate** —
|
||||
which grants those permissions and spawns the isolated process. A page plugin
|
||||
then appears in the top navigation; a widget appears on the dashboard.
|
||||
Activation is the **toggle** on the installed row. Flipping it on grants the
|
||||
permissions you already reviewed and spawns the isolated process; flipping it off
|
||||
stops that process immediately while keeping the plugin's data. A page plugin then
|
||||
appears in the top navigation; a widget appears on the dashboard. There is no
|
||||
separate "Activate" button or second consent screen — you reviewed the permissions
|
||||
before installing.
|
||||
|
||||
## Managing plugins
|
||||
## Managing a plugin
|
||||
|
||||
- **Disable** stops the process immediately; the plugin keeps its data.
|
||||
- **Uninstall** removes the code and lets you keep or delete the plugin's data.
|
||||
- **View errors** shows the plugin's own error log (crashes, failed requests).
|
||||
The **⋯** menu on each row:
|
||||
|
||||
- **Restart** — stop and re-spawn the process (shown only while active).
|
||||
- **View error log** — the plugin's own crash/failed-request log.
|
||||
- **Source repository** — opens the plugin's GitHub repo (registry installs only).
|
||||
- **Delete** — uninstalls: removes the code and lets you keep or delete its data.
|
||||
|
||||
## Updating a plugin
|
||||
|
||||
When the registry lists a newer version, an **Update to vX.Y.Z** pill appears on
|
||||
the row, and an **Update all** bar summarises how many are available.
|
||||
|
||||
Updating swaps in the new code and, by default, transparently restarts the plugin
|
||||
on it. But if the new version declares **more permissions or new outbound hosts**,
|
||||
TREK installs the new code and **leaves the plugin off**, then shows a
|
||||
**re-consent dialog** listing exactly the new permissions and hosts. The plugin
|
||||
only runs again once you approve — an update can never silently widen what a
|
||||
plugin may do. Choosing "Later" keeps the new code installed but inactive.
|
||||
|
||||
## Building your own
|
||||
|
||||
See [[Plugin Development|Plugin-Development]] and the `trek-plugin-sdk` package.
|
||||
Plugins are built with the **plugin SDK** and its `trek-plugin` CLI. The CLI
|
||||
turns a built plugin directory into a publishable artifact and the ready-to-PR
|
||||
registry entry, so you never hand-compute a SHA-256 or hand-write registry JSON:
|
||||
|
||||
## Kill switch
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `trek-plugin validate [dir]` | Runs the manifest + layout checks locally (a subset of registry CI, which additionally verifies the release, the artifact SHA-256, and the README over the network). |
|
||||
| `trek-plugin pack [dir] [--out plugin.zip] [--json]` | Builds `plugin.zip` in the installer's exact layout and prints its SHA-256 + byte size. Refuses native binaries; `docs/` is intentionally not shipped (the store fetches the screenshot from your repo). |
|
||||
| `trek-plugin entry --repo <o/n> --tag <vX> [--zip z] [--merge entry.json] [--out f]` | Emits the registry entry — `commitSha`, `downloadUrl`, `sha256`, `size` and `minTrekVersion` (derived from the manifest `trek` range) all filled in. `--merge` prepends the new version onto an existing entry for updates. |
|
||||
| `trek-plugin release [dir] --repo <o/n> --tag <vX>` | The one-shot: `pack` → create the GitHub release → print the entry. |
|
||||
|
||||
Set `TREK_PLUGINS_ENABLED=false` to disable the entire subsystem instantly —
|
||||
installed plugins stay on disk, deactivated and harmless.
|
||||
Run them via `npx trek-plugin …`. See [[Plugin Development|Plugin-Development]]
|
||||
for the SDK and manifest, and [[Publishing a Plugin|Plugin-Publishing]] for the
|
||||
registry PR flow. Publishing no longer reserves any namespaces — any unique slug
|
||||
is fine — but an `id` stays bound to the GitHub owner who first registered it, so
|
||||
nobody can repoint an existing plugin. Entries may optionally carry an author
|
||||
signing key (`authorPublicKey` + a per-version `signature`) for offline signature
|
||||
verification on top of the SHA-256 pin.
|
||||
|
||||
Reference in New Issue
Block a user