docs(plugins): plugin wiki + reference plugin (M7)

- Wiki pages (sync to the GitHub wiki on push to main): Plugins overview + trust
  model, Plugin Development (SDK, definePlugin, ctx, routes/jobs, the client
  bridge, testing with the mock host), Plugin Permissions reference, and
  Publishing (registry PR + CI gates + provenance). Linked from the sidebar.
- Reference plugin plugin-sdk/examples/trip-countdown: a complete, minimal-
  permission widget (reads trip data through ctx, renders in the sandboxed
  iframe via the bridge, filled-in README). Validated in the SDK test suite so it
  passes the exact gate authors face.
This commit is contained in:
mauriceboe
2026-07-03 17:55:07 +02:00
committed by Maurice
parent af73943be8
commit 660e4b9dfc
9 changed files with 511 additions and 0 deletions
@@ -0,0 +1,39 @@
# Trip Countdown
> A dashboard widget that counts down the days to your next trip.
![Trip Countdown widget on the dashboard](./docs/screenshot.png)
## What it does
Trip Countdown adds a small dashboard widget showing how many days remain until
your upcoming trip starts. When a trip is under way it shows "in progress"
instead. It's the reference plugin for the TREK plugin system — a minimal but
complete widget that reads trip data through the sandbox and renders it in an
isolated iframe.
## Screenshots
![The widget counting down to a trip](./docs/screenshot.png)
## Permissions
| Permission | Why this plugin needs it |
|---|---|
| `db:read:trips` | To read the current trip's start date so it can compute the countdown. The read is membership-checked by TREK, so the widget only ever sees a trip you already have access to. |
It requests **no** network access and stores **no** data of its own.
## Setup
None. Enable plugins on your instance (`TREK_PLUGINS_ENABLED=true`), install and
activate Trip Countdown, and the widget appears on your dashboard.
## Building
The widget ships plain JS/HTML — no build step. `server/index.js` is the plugin
entry; `client/index.html` is the widget iframe.
## License
MIT — see the TREK repository.
@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Trip Countdown</title>
<style>
:root { color-scheme: light dark; }
body { margin: 0; font: 14px system-ui, sans-serif; display: grid; place-items: center; height: 100vh;
background: transparent; color: #111827; }
body[data-theme="dark"] { color: #e5e7eb; }
.card { text-align: center; padding: 16px; }
.days { font-size: 40px; font-weight: 800; letter-spacing: -0.02em; }
.label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.6; margin-top: 2px; }
.name { margin-top: 6px; opacity: 0.85; }
.empty { opacity: 0.6; }
</style>
</head>
<body>
<div class="card" id="root"><span class="empty">Loading…</span></div>
<script>
var ctx = null
var reqSeq = 0
function send(msg) { window.parent.postMessage(msg, '*') }
function render(data) {
var root = document.getElementById('root')
if (!data || !data.trip) { root.innerHTML = '<span class="empty">No upcoming trip</span>'; return }
var d = data.trip.days
root.innerHTML = d == null
? '<div class="name">' + escapeHtml(data.trip.name) + '</div><div class="label">in progress</div>'
: '<div class="days">' + d + '</div><div class="label">days to go</div><div class="name">' + escapeHtml(data.trip.name) + '</div>'
}
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, function (c) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c] }) }
function load() {
var id = 'r' + (++reqSeq)
send({ type: 'trek:invoke', requestId: id, sub: '/next?tripId=' + (ctx && ctx.tripId ? ctx.tripId : ''), method: 'GET' })
window._pending = id
}
window.addEventListener('message', function (e) {
var m = e.data
if (!m || typeof m !== 'object') return
if (m.type === 'trek:context') {
ctx = m
document.body.dataset.theme = m.theme
load()
} else if (m.type === 'trek:response' && m.requestId === window._pending) {
render(m.data)
} else if (m.type === 'trek:error' && m.requestId === window._pending) {
document.getElementById('root').innerHTML = '<span class="empty">Unavailable</span>'
}
})
// Ask TREK for context (theme, current trip id) — it replies with trek:context.
send({ type: 'trek:ready' })
</script>
</body>
</html>
@@ -0,0 +1,43 @@
// Trip Countdown — reference widget plugin.
//
// Demonstrates the full author path: definePlugin, a permission-gated route that
// reads trip data through ctx, and a response the client iframe renders. Runs
// isolated; ctx is the only way to reach TREK.
const { definePlugin } = require('trek-plugin-sdk')
/** Days (integer, >= 0) between now and an ISO date, or null if past/absent. */
function daysUntil(iso) {
if (!iso) return null
const then = new Date(iso).getTime()
if (Number.isNaN(then)) return null
const diff = Math.ceil((then - Date.now()) / 86_400_000)
return diff >= 0 ? diff : null
}
module.exports = definePlugin({
routes: [
{
method: 'GET',
path: '/next',
auth: true,
async handler(req, ctx) {
// The client passes the dashboard's current trip id; the host enforces that
// req.user actually has access to it, so a plugin can't read foreign trips.
const tripId = Number(req.query.tripId)
if (!tripId) return { status: 200, headers: json(), body: JSON.stringify({ trip: null }) }
const t = await ctx.trips.getById(tripId, req.user.id)
const days = daysUntil(t && (t.start_date || t.startDate))
return {
status: 200,
headers: json(),
body: JSON.stringify({ trip: t ? { name: t.name, days } : null }),
}
},
},
],
})
function json() {
return { 'content-type': 'application/json' }
}
@@ -0,0 +1,21 @@
{
"id": "trip-countdown",
"name": "Trip Countdown",
"version": "1.0.0",
"apiVersion": 1,
"author": "TREK",
"description": "A dashboard widget counting down the days to your next trip.",
"homepage": "https://github.com/mauriceboe/TREK",
"license": "MIT",
"icon": "CalendarClock",
"type": "widget",
"trek": ">=3.2.0 <4.0.0",
"nativeModules": false,
"permissions": ["db:read:trips"],
"capabilities": {
"widget": { "title": "Trip Countdown", "defaultSize": "small" }
},
"routes": [
{ "method": "GET", "path": "/next", "auth": true }
]
}