docs(wiki): bring the wiki in line with 4.0.0 (#2034)

* docs(wiki): correct the install and operations pages for 4.0.0

Quick Start passed docker an empty ENCRYPTION_KEY: an assignment prefix is not
expanded on the same command line, so the server silently generated its own key
instead of taking the one the page told you to set. The tag examples pinned to
the 3.x line, the documented tmpfs size did not match the shipped compose file,
and several .env values were described as reaching the container when compose
never interpolates them.

* docs(wiki): match the MCP reference to the registered surface

The scope table was missing the whole Collections group and the totals said 27
scopes across 13 groups where the code registers 29 across 14. The Cloudflare
link on the setup page pointed at an anchor on its own page instead of the
heading on Troubleshooting.

* docs(wiki): correct the plugin and admin pages

Three links to Plugin-Development carried a .md suffix, which does not resolve on
the wiki. The admin pages described tabs, permissions and storage behaviour that
had moved on.

* docs(wiki): bring the feature pages back in line with the app

Costs still documented the old panel, where categories could be added, renamed,
reordered and deleted; they are fixed now and the panel is a day-grouped ledger
with settlements. Trip sharing, journeys, maps, packing, reservations and the
account pages had all drifted in the same way.

* docs(wiki): cover the pages the first pass did not reach

The MCP catalog listed 168 of the 201 registered tools; the settlement tools and
rate_place were missing here and the Collections family from the addon page.
Several resource rows claimed a trips:* scope where the handler asks for
trips:read.

* docs(wiki): tighten the corrections a review picked apart

The audit-log resource is the row id, not the credential id; an admin password
reset does not bump password_version the way the page implied; and a few of the
new paragraphs described behaviour more confidently than the code supports.

* docs(wiki): fix what a last read-through caught

Costs shows every figure in your display currency, not the trip's, and an
expense with no payer still counts against the members it is split between.
Studio's cutoff is a 1024px viewport rather than phones alone, and the
Integrations tab is gated on the providers an admin has configured.

* docs(wiki): the last few the review caught

A passkey satisfies the MFA policy everywhere except the WebSocket handshake,
which still asks for TOTP, so a passkey-only account under that policy gets no
live updates. Mobile optimizes a day from two places, not three. The ctx.db row
carried an unescaped pipe that split the table, and the Backups table linked
with a wiki link whose pipe did the same.
This commit is contained in:
Maurice
2026-08-25 12:36:52 +02:00
committed by GitHub
parent dc74d59b11
commit 7da0080557
77 changed files with 949 additions and 466 deletions
+75 -19
View File
@@ -237,14 +237,14 @@ module.exports = definePlugin({
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 scaffold writes no `routes` block into `trek-plugin.json`, and the manifest
parser would ignore one if you added it.
### The `ctx` object
| Area | Methods | Requires |
|---|---|---|
| `ctx.db` | `query(sql, …args)` / `exec(sql, …args)` / `migrate(id, sql)` / `tx(ops)` against your **own** SQLite file. `tx([{sql, args?}, …])` runs up to 100 statements in one transaction (all commit or all roll back; reads see the batch's own earlier writes) → `{ results: [{changes?}|{rows?}, …] }` | `db:own` |
| `ctx.db` | `query(sql, …args)` / `exec(sql, …args)` / `migrate(id, sql)` / `tx(ops)` against your **own** SQLite file. `tx([{sql, args?}, …])` runs up to 100 statements in one transaction (all commit or all roll back; reads see the batch's own earlier writes) → `{ results: [{changes?}\|{rows?}, …] }`. Your file is capped at **256 MB** (a write past it fails `SQLITE_FULL`, contained to your plugin) and a single result set at **100,000 rows** — page your reads instead of materialising a cartesian product | `db:own` |
| `ctx.trips` | `getById` / `getPlaces` / `getReservations` / `getDays` / `getAccommodations` / `listMine()` — enumerate every trip the acting user can access (membership-checked). `getDays` includes each day's `assignments` + `notes_items`; `getReservations` includes `endpoints` + `day_positions` | `db:read:trips` |
| `ctx.trips.update(tripId, fields)` | update trip fields (title/dates/currency/reminder_days/…) | `db:write:trips` |
| `ctx.trips.create(input)` | create a **new trip owned by the acting user** (importers) — `title` required, plus `description?`/`start_date?`/`end_date?`/`currency?`/`reminder_days?`/`day_count?` | `db:create:trips` (+ `trip_create`) |
@@ -333,15 +333,56 @@ validated against TREK's budget schema, and a successful create broadcasts the s
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.
The proxy forwards `{ method, path, query, body, headers, user }`. **`req.headers`
is populated ONLY on `auth: false` routes** (an authenticated route gets `{}`) and
only an explicit, credential-free **allowlist** — the common provider signature +
event headers (`stripe-signature`, `x-hub-signature-256`, `svix-signature`,
`x-gitlab-event`, `content-type`, `user-agent`, …). **`Cookie`, `Authorization`,
`X-Socket-Id` and every session/forwarded-auth header are stripped** and never reach
your code, so a forwarded header can't leak a TREK session. To trust a webhook,
verify the provider's signature over the raw body against a secret you hold in
`ctx.config` (admin-set instance setting) or `ctx.settings` (per-user).
The proxy forwards `{ method, path, query, body, rawBodyBase64, headers, user }`.
**`req.headers` is populated ONLY on `auth: false` routes** (an authenticated route
gets `{}`) and only an explicit, credential-free **allowlist** — the common provider
signature + event headers (`stripe-signature`, `x-hub-signature-256`,
`svix-signature`, `x-gitlab-event`, `content-type`, `user-agent`, …). **`Cookie`,
`Authorization`, `X-Socket-Id` and every session/forwarded-auth header are stripped**
and never reach your code, so a forwarded header can't leak a TREK session.
**`req.rawBodyBase64` is webhook-only in the same way** — the exact request bytes,
base64-encoded, on `auth: false` routes, and absent on an authenticated route, which
never needs them. To trust a webhook, verify the provider's signature over *those*
bytes against a secret you hold in `ctx.config` (admin-set instance setting) or
`ctx.settings` (per-user). Never re-serialize `req.body` for the check:
`JSON.stringify` won't reproduce the key order, whitespace and unicode escaping the
sender signed, so the HMAC won't match.
The snippet below handles two things, and yours has to as well. TREK only keeps the
raw bytes when a body parser ran, i.e. for `application/json` and
`application/x-www-form-urlencoded` — a webhook posted as `text/plain` or a GET
callback with no body arrives with `rawBodyBase64: null`, so **fail closed** instead
of running the HMAC over an empty buffer, which is a constant anyone can forge. And
the request body ceiling is 100 kB; a larger payload is rejected before your route
runs.
```js
const crypto = require('node:crypto')
if (typeof req.rawBodyBase64 !== 'string') return { status: 400, body: { error: 'no raw body' } }
const raw = Buffer.from(req.rawBodyBase64, 'base64')
const mac = crypto.createHmac('sha256', ctx.config.webhookSecret).update(raw).digest()
const sent = Buffer.from(String(req.headers['x-hub-signature-256'] || '').replace(/^sha256=/, ''), 'hex')
if (sent.length !== mac.length || !crypto.timingSafeEqual(mac, sent)) return { status: 401, body: { error: 'bad signature' } }
```
### Runtime limits
The host enforces these on every plugin. They are generous for real work and only
bite a runaway one, but they are exactly what a plugin that loops over `ctx.*` or
stores blobs in `ctx.db` runs into, so build against them.
| Area | Limit |
|---|---|
| every `ctx.*` call | burst 60, sustained 20/s, 16 in-flight per plugin; a throttled call is refused with `HOST_ERROR: rate limit exceeded — slow down ctx.* calls`. Tunable with `TREK_PLUGIN_RPC_BURST` / `TREK_PLUGIN_RPC_PER_SEC` / `TREK_PLUGIN_RPC_INFLIGHT` (see [Environment-Variables](Environment-Variables#plugins)) |
| `ctx.db` | 256 MB per plugin, one result set capped at 100,000 rows |
| event subscriptions | 200 events buffered per plugin while it restarts, dropped unreplayed after 15 minutes |
| the plugin process | 300 MB RSS (`TREK_PLUGIN_MAX_RSS_MB`) — the child is killed past it; auto-disabled with status `error` after 5 crashes inside a 5-minute window |
The daily `ctx.ai` / `ctx.notify` budgets and the `ctx.meta` quotas are in
[Testing without a running TREK](#testing-without-a-running-trek), because the mock
host enforces those too.
## Writing the client (page / widget)
@@ -819,9 +860,19 @@ also being given the right to read their trips as them.
Notes:
- **`title`** names the column in the notification preferences matrix (defaults to your
plugin's name). **`events`** may *narrow* which events the channel carries; the default is
every non-admin event. Admin-scoped events (`version_available`) are never deliverable to a
plugin channel — those go over the admin's own credentials.
plugin's name). **`events`** may *narrow* which events the channel carries; omit it and
the channel carries all ten deliverable events: `trip_invite`, `booking_change`,
`trip_reminder`, `todo_due`, `vacay_invite`, `collection_invite`, `photos_shared`,
`collab_message`, `packing_tagged`, `plugin_notification`. The SDK exports the same list
as `CHANNEL_EVENTS`, and both `trek-plugin validate` and the installer refuse anything
outside it with `capabilities.notificationChannel.events: "x" is not a plugin-deliverable
event`.
The four events TREK sends that a plugin channel never carries are the admin-scoped
`version_available` and `replica_failure` (those go over the admin's own credentials),
the in-app-only `synology_session_cleared`, and `vacay_share`. Don't read the set off the
table in [Notifications](Notifications) — that one lists what a user can toggle, not what
a channel can carry.
- **Configured-ness is inferred, not asked.** A user's column is "not configured" until every
`required`, `scope:'user'` field has a value. A plugin with no required user fields counts as
configured for everyone (an instance-wide channel, e.g. a shared workspace webhook).
@@ -1095,10 +1146,14 @@ import { createMockHost } from 'trek-plugin-sdk/testing'
const { ctx, broadcasts } = createMockHost({
grants: ['db:read:trips'],
trips: { 1: { members: [42], data: { id: 1, name: 'Japan' } } },
actingUserId: 42, // the user every read is checked against
trips: {
1: { members: [42], data: { id: 1, name: 'Japan' } },
2: { members: [99], data: { id: 2, name: 'Peru' } },
},
})
await ctx.trips.getById(1, 42) // ok — member
await expect(ctx.trips.getById(1, 99)).rejects… // RESOURCE_FORBIDDEN
await ctx.trips.getById(1) // ok — member
await expect(ctx.trips.getById(2)).rejects… // RESOURCE_FORBIDDEN
await expect(ctx.db.query('SELECT 1')).rejects… // PERMISSION_DENIED (no db:own)
```
@@ -1198,6 +1253,7 @@ what your manifest declares.
| `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. |
| `tags` | string[] | store keywords, copied verbatim onto the registry entry. Each must be a lowercase slug of 2–24 chars (`[a-z0-9-]`), at most 8 — `validate` rejects a capital or a space, because the registry's schema does. |
| `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. |
@@ -1206,7 +1262,7 @@ what your manifest declares.
| `capabilities.tripPage` | object | `{ replaces?, position? }` for `trip-page` plugins — `replaces` names core planner tabs to hide while active (`transports`, `buchungen`, `listen`, `finanzplan`, `dateien`, `collab`; never `plan`), `position` is the tab's 0-based index in the bar (0–50; omitted = appended). |
| `actions` | array | Buttons on the plugin's own settings page — `{ key, label, hint?, danger? }` (max 8). Implement each as `actions[key](ctx)` on the definition. **User-initiated**, so `ctx.settings.get()` returns the clicking user's value. See [Settings-page actions](#settings-page-actions). |
| `operatorEgress` | boolean | The plugin talks to a **self-hosted** service whose hostname only the operator knows. The admin adds the real hosts after install (Admin → Plugins → Allowed hosts) and the runtime unions them into the egress allow-list. Requires an `http:outbound` permission, and is the only way to declare one with an empty `egress[]`. See [Operator-supplied egress hosts](#operator-supplied-egress-hosts-operatoregress). |
| `capabilities.notificationChannel` | object | `{ title?, events? }` for a plugin implementing the `notificationChannel` hook — `title` names the column in the notification preferences matrix (default: the plugin's `name`), `events` **narrows** which events the channel carries (default: every non-admin event; admin-scoped events are never deliverable). Requires the `hook:notification-channel` permission. See [Notification channels](#notification-channels). |
| `capabilities.notificationChannel` | object | `{ title?, events? }` for a plugin implementing the `notificationChannel` hook — `title` names the column in the notification preferences matrix (default: the plugin's `name`), `events` **narrows** which events the channel carries (default: all ten plugin-deliverable events; `events` may only narrow that set). Requires the `hook:notification-channel` permission. See [Notification channels](#notification-channels). |
| `capabilities.routeProfiles` | array | up to 3 `{ id, label, icon? }` entries for a plugin implementing the `routeProvider` hook — each becomes a selectable mode in the planner's route toggle (next to Driving/Walking). `id` is lowercase `[a-z][a-z0-9-]` (max 24 chars) and is what `getRoute` receives as `request.profile`; `label` (≤40 chars) is shown to the user. Requires the `hook:route-provider` permission. |
| `capabilities.provides` | string[] | function names this plugin exposes to its dependents via `ctx.plugins.call` (see [Talking to other plugins](#talking-to-other-plugins)). |
| `capabilities.emits` | string[] | event names this plugin publishes to its dependents via `ctx.events.emit`. |