mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
Fix/plugin audit findings (#2031)
* fix(plugins): enforce manifest apiVersion at install and activation * test(plugins): exercise the absent-apiVersion default * feat(plugins): generate the core-event catalog into host-facts * docs(plugins): explain the packing.listBags write-permission binding * test(plugins): cover apiVersion upload refusal; tidy docblocks and generator header * fix(plugin-sdk): validate settings options and oauth like the host * fix(plugin-sdk): model the full manifest shape and stop dropping fields * fix(plugin-sdk): drop non-string routeProfiles icons like the host * docs(plugin-sdk): document host limits, oauth contract, and ignored asUserId * feat(plugin-sdk): enforce daily budgets and meta quotas in the mock host * feat(plugin-sdk): expose the generated core-event catalog * feat(plugin-sdk): scaffold trek range >=3.4.0 <5.0.0 ahead of TREK 4.0 * feat(plugin-sdk): scaffold the oauth broker settings for oauth:client plugins * chore(plugin-sdk): fix stale mirror docs and the colliding permissions export * docs(plugin-sdk): add the rate-limit string; restore trek docblock; match host meta sizing * feat(plugin-sdk): raise the scaffold trek floor to 4.0.0 * docs(wiki): bring the plugin pages in line with the current runtime * docs: bring the workspace CLAUDE.md guides up to date and make them committable * feat(plugin-sdk): roll back failed publishes, add unrelease, sync the fork, retro-sign updates * docs(wiki): document the publish rollback, unrelease, fork sync, and retro-signing * chore(plugin-sdk): 1.6.0
This commit is contained in:
+39
-21
@@ -50,9 +50,11 @@ my-plugin/
|
||||
.gitattributes # LF everywhere, so the packed artifact's sha256 is reproducible
|
||||
```
|
||||
|
||||
The scaffold declares `"trek": ">=3.4.0 <4.0.0"` — the `ctx` namespaces it is written
|
||||
against only exist from 3.4.0, and claiming an older floor would let the plugin install
|
||||
on a host where they are `undefined`.
|
||||
The scaffold declares `"trek": ">=4.0.0 <5.0.0"` — the host surface it is written
|
||||
against is the TREK 4 surface, and claiming an older floor would let the plugin
|
||||
install on a host where parts of it are missing. Picking `oauth:client` in the wizard
|
||||
also scaffolds the five instance-scope settings the OAuth broker reads (see
|
||||
[Host-brokered OAuth](#host-brokered-oauth-ctxoauth)).
|
||||
|
||||
What you get **runs and packs immediately**, but it does **not** pass `validate` yet:
|
||||
the README is still a template and there is no screenshot. That is deliberate — those
|
||||
@@ -137,11 +139,9 @@ permissions still requires explicit re-consent).
|
||||
|
||||
## The plugin types
|
||||
|
||||
- **integration** — background logic (jobs, routes) with no UI of its own. Most
|
||||
provider hooks are **live** (placeDetailProvider, warningProvider,
|
||||
tableContributor, mapMarkerProvider, mapLayerProvider, pdfSectionProvider,
|
||||
atlasLayerProvider, journalEntryProvider); only `photoProvider` / `calendarSource` are declared in the
|
||||
SDK but **not yet wired into the host** — see [Provider hooks](#provider-hooks).
|
||||
- **integration** — background logic (jobs, routes) with no UI of its own. Every
|
||||
provider hook is **live** — from placeDetailProvider and warningProvider through
|
||||
photoProvider (Memories) and calendarSource — see [Provider hooks](#provider-hooks).
|
||||
- **page** — adds a nav entry that opens a full-page sandboxed iframe.
|
||||
- **widget** — adds a card to the dashboard (`sidebar` slot), a hero-bar overlay
|
||||
(`hero` slot), a panel inside the trip planner's **place-detail** view
|
||||
@@ -251,7 +251,7 @@ declaration for readers — the manifest parser does not consume it.
|
||||
| `ctx.places` | `create(tripId, fields)` / `update(tripId, placeId, fields)` / `delete(tripId, placeId)` | `db:write:places` |
|
||||
| `ctx.days` | `create(tripId, {date?, notes?})` / `update(tripId, dayId, {notes?, title?})` / `delete(tripId, dayId)` | `db:write:days` |
|
||||
| `ctx.itinerary` | `assign(tripId, dayId, placeId, notes?)` / `unassign(tripId, assignmentId)` — place↔day | `db:write:itinerary` |
|
||||
| `ctx.meta` | `get` / `set` / `list` / `delete` your **own** namespaced data on a `trip`/`place`/`day` (enrich core entities without forking the schema) | `db:meta` |
|
||||
| `ctx.meta` | `get` / `set` / `list` / `delete` your **own** namespaced data on a `trip`/`place`/`day`/`reservation`/`accommodation` (enrich core entities without forking the schema) | `db:meta` |
|
||||
| `ctx.packing` | `list(tripId)` — a trip's packing items (membership-checked, respects private-item visibility) | `db:read:packing` |
|
||||
| `ctx.files` | `list(tripId)` — a trip's files, trash excluded (membership-checked) | `db:read:files` |
|
||||
| `ctx.files.getContent` | `getContent(tripId, fileId)` → `{ name, mimetype, size, content_base64 }` — a file's bytes as base64 (10MB cap; trashed files refused) | `db:read:files:content` |
|
||||
@@ -681,7 +681,7 @@ simply ignore the flag, so declaring it never breaks an install.
|
||||
|
||||
A plugin can act as an OAuth *client* of a third-party service **without ever handling the secrets**. The **host** runs the entire flow — authorize → callback → token exchange → refresh — with PKCE (S256) and a `state` check, and holds the tokens. The plugin only triggers "connect" and reads a **short-lived access token** at runtime.
|
||||
|
||||
**Setup.** Declare the provider as `scope: "instance"` settings the admin fills in: `oauth_authorize_url`, `oauth_token_url`, `oauth_scopes` (optional), plus the two `secret: true` fields `oauth_client_id` and `oauth_client_secret`. (A settings field may also carry an `oauth: { initPath, callbackPath }` block.)
|
||||
**Setup.** Declare the provider as `scope: "instance"` settings the admin fills in: `oauth_authorize_url`, `oauth_token_url`, `oauth_scopes` (optional), plus `oauth_client_id` and the `secret: true` `oauth_client_secret`. (A settings field may also carry an `oauth: { initPath, callbackPath }` block.) `trek-plugin create` scaffolds all five for you when you pick the `oauth:client` permission.
|
||||
|
||||
**Connecting.** A user connects under **Settings → Plugins → Connect**, which sends them to the provider's authorize page. The callback returns to `…/api/plugin-oauth/<id>/callback`; the host verifies the `state` (single-use, 10-minute TTL, bound to that user — CSRF defence), exchanges the code and stores the tokens **per user, encrypted at rest**.
|
||||
|
||||
@@ -750,7 +750,7 @@ module.exports = definePlugin({
|
||||
| `pdfSectionProvider.getSections(tripId, ctx)` → `PdfSection[]` | `hook:pdf-section-provider` | **live** — text-only sections appended to the trip PDF export. Each is `{title, paragraphs?, table?}`; the host escapes and lays everything out itself (no markup ever reaches the document), caps counts (≤5 sections/plugin, ≤20 paragraphs, ≤8 headers, ≤50 rows) + lengths (title 120, paragraph 2000, header 60, cell 200) and clips rows to the header width. Also `GET /api/pdf-sections/:tripId` |
|
||||
| `atlasLayerProvider.getLayers(ctx)` → `AtlasLayer[]` | `hook:atlas-layer-provider` | **live** — country tint layers drawn over the Atlas world map (wishlists, advisories, …). **User-scoped**: the host binds the acting user, the hook takes no target parameter. Each layer is `{id, name?, countries: [{code, tone?, label?}]}`; codes must be ISO-3166 alpha-2 (uppercase-coerced), tone is enum-whitelisted, counts capped (≤3 layers/plugin, ≤300 countries/layer). Declarative only — plugin JS never runs on the map canvas. Also `GET /api/atlas-layers` |
|
||||
| `journalEntryProvider.getRows(entryId, ctx)` → `{ label, value?, url? }[]` | `hook:journal-entry-provider` | **live** — extra rows rendered under a journal entry card (needs the Journey addon; the entry's journey is access-checked like the journal detail routes). Same hardening as place details plus server-side normalization: ≤12 rows/plugin, label ≤60, value ≤200, url http/https/mailto-only. Also `GET /api/journal-entry-rows/:entryId` |
|
||||
| `tripCardProvider.getCards(tripIds, ctx)` → `TripCardContribution[]` | `hook:trip-card-provider` | **live** — small badges on the dashboard trip cards. Called ONCE with all visible `tripIds` (each already access-checked for the acting user), returns `{ tripId, id, label, value?, icon?, tone?, url? }[]`; the host bounds every field (label 64, value 256, tone enum, url http/https/mailto-only), caps the count (≤40/plugin) and drops any badge whose `tripId` wasn't requested. Declarative only. Also `GET /api/trip-card-contributions?tripIds=…` |
|
||||
| `tripCardProvider.getCards(tripIds, ctx)` → `TripCardContribution[]` | `hook:trip-card-provider` | **live** — small badges on the dashboard trip cards. Called ONCE with all visible `tripIds` (each already access-checked for the acting user), returns `{ tripId, id, label, value?, icon?, tone?, url? }[]`; the host bounds every field (label 64, value 256, tone enum, url http/https/mailto-only), caps the count (≤4 badges per trip, ≤240 total per plugin) and drops any badge whose `tripId` wasn't requested. Declarative only. Also `GET /api/trip-card-contributions?tripIds=…` |
|
||||
| `photoProvider.search(query, {page, limit}, ctx)` / `.getById(id, ctx)` | `hook:photo-provider` | **live** — plugin photo sources aggregated at `GET /api/plugin-photos/search` (+ `/sources`, `/item`) for the picker. Each `{id, title?, thumbnailUrl, fullUrl, takenAt?}`; thumbnail/full URLs must be http/https, per-source count capped, a failing source skipped |
|
||||
| `calendarSource.getName(ctx)` / `.getEvents(userId, start, end, ctx)` | `hook:calendar-source` | **live** — plugin calendar events aggregated for the signed-in user at `GET /api/plugin-calendar?start=&end=`. Each `{id, title, start, end, allDay}` (ISO dates); count capped, a failing source skipped |
|
||||
| `notificationChannel.send(msg, config, ctx)` / `.test(config, ctx)` | `hook:notification-channel` | **live** — registers a new notification channel. **Userless** (see below). See [Notification channels](#notification-channels) |
|
||||
@@ -962,8 +962,10 @@ Delivery is fire-and-forget on a short timeout, so a slow subscriber never block
|
||||
core write. Because there's no user, trip reads (`ctx.trips.*`) are refused inside a
|
||||
handler — beyond the snapshot, use the plugin's own `ctx.db`, `ctx.ws.*`, or an
|
||||
outbound call. A plugin's own `plugin:*` broadcasts are never delivered back, so
|
||||
handlers can't loop. Common events: `place:*`, `day:*`, `assignment:*`, `budget:*`,
|
||||
`file:*`, `accommodation:*`.
|
||||
handlers can't loop. Event names follow `<family>:<verb>` (`place:created`,
|
||||
`day:updated`, `file:*`, `assignment:*`, `budget:*`, `accommodation:*`, …); the SDK
|
||||
exports the authoritative family list and the snapshot-grant mapping as
|
||||
`EVENT_FAMILIES` and `EVENT_SNAPSHOT_GRANT`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -1105,6 +1107,13 @@ integration test for real SQL. To test inter-plugin calls, pass
|
||||
`pluginExports: { koffi: { convert: (args) => … } }` and assert on `mock.emitted`
|
||||
for anything your plugin publishes via `ctx.events.emit`.
|
||||
|
||||
The mock also enforces the host's operational limits, so a test can prove your
|
||||
plugin backs off correctly: the **daily AI and notification budgets** (defaults
|
||||
200 / 100 per day; configure with `aiPerDay` / `notifyPerDay` — `0` disables the
|
||||
broker entirely, the exhausted call fails with the host's exact error) and the
|
||||
**`ctx.meta` quotas** (≤256-char keys, ≤64 KB serialized values, ≤100 keys per
|
||||
entity).
|
||||
|
||||
### Driving your plugin's handlers
|
||||
|
||||
`createMockHost` also gives you `run(def)` — the other half of a test. Where the
|
||||
@@ -1182,9 +1191,9 @@ what your manifest declares.
|
||||
| `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`. |
|
||||
| `apiVersion` | number | plugin API version (currently `1`; `PLUGIN_API_VERSION`). Defaults to `1`; must be a positive integer, and a version newer than the host supports is refused at install and won't activate (`API_VERSION_INCOMPATIBLE`). |
|
||||
| `type` | string, **required** | `integration` \| `page` \| `widget` \| `trip-page`. |
|
||||
| `trek` | string, **required** | the TREK versions this plugin supports, as a semver **range**: `">=3.4.0 <4.0.0"` (what the scaffold writes). Must be *satisfiable* (`">=4.0.0 <3.0.0"` parses but nothing can satisfy it — rejected). **Enforced at install and at activation** (see below); it is copied verbatim onto the registry entry, which is the entry's only compatibility field. Keep it **bounded** — an unbounded range claims support for TREK versions that do not exist yet. |
|
||||
| `trek` | string, **required** | the TREK versions this plugin supports, as a semver **range**: `">=4.0.0 <5.0.0"` (what the scaffold writes). Must be *satisfiable* (`">=4.0.0 <3.0.0"` parses but nothing can satisfy it — rejected). **Enforced at install and at activation** (see below); it is copied verbatim onto the registry entry, which is the entry's only compatibility field. Keep it **bounded** — an unbounded range claims support for TREK versions that do not exist yet. |
|
||||
| `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. |
|
||||
@@ -1232,9 +1241,10 @@ build arg defaults to the literal `dev` — has nothing to compare a range again
|
||||
the check is skipped and an unversioned build installs anything. Plugins should still
|
||||
guard optional `ctx.*` namespaces.
|
||||
|
||||
**Permissions** — the commonly-used core subset below; the **full list of ~50**
|
||||
**Permissions** — the commonly-used core subset below; the **full list of 63**
|
||||
(all read/write scopes, the notify/ai/oauth brokers, every provider hook) lives in
|
||||
**[[Plugin Permissions|Plugin-Permissions]]**. Unknown values are rejected at activation.
|
||||
**[[Plugin Permissions|Plugin-Permissions]]**. Unknown values are rejected at install
|
||||
(and by `trek-plugin validate` and registry CI, which check against the same list).
|
||||
|
||||
| Permission | Grants |
|
||||
|---|---|
|
||||
@@ -1270,7 +1280,8 @@ guard optional `ctx.*` namespaces.
|
||||
| `hook:journal-entry-provider` | `hooks.journalEntryProvider` — extra rows on a journal entry card |
|
||||
| `hook:trip-card-provider` | `hooks.tripCardProvider` — small badges on the dashboard trip cards (`getCards(tripIds, ctx)` → `{ tripId, label, value?, icon?, tone?, url? }[]`; host bounds every field + access-checks each tripId) |
|
||||
| `hook:user-data` | `deleteUserData` / `exportUserData` handlers — honour GDPR erasure (durable, retried) and data-export for a deleted/requesting user (userless; own db only) |
|
||||
| `hook:photo-provider` / `hook:calendar-source` | reserved (see [Provider hooks](#provider-hooks)) |
|
||||
| `hook:photo-provider` | `hooks.photoProvider` — a photo source for Memories, aggregated at `GET /api/plugin-photos/search` (see [Provider hooks](#provider-hooks)) |
|
||||
| `hook:calendar-source` | `hooks.calendarSource` — calendar events for the signed-in user, aggregated at `GET /api/plugin-calendar` (see [Provider hooks](#provider-hooks)) |
|
||||
|
||||
> There is **no `ws:broadcast:*`** — use `ws:broadcast:trip` and/or
|
||||
> `ws:broadcast:user` explicitly.
|
||||
@@ -1286,7 +1297,7 @@ guard optional `ctx.*` namespaces.
|
||||
| `required` | boolean. |
|
||||
| `secret` | boolean — encrypted at rest, decrypted only into `ctx.config`. |
|
||||
| `placeholder`, `hint` | form hints. |
|
||||
| `options` | `[{ value, label }]` for select inputs. |
|
||||
| `options` | `[{ value, label }]` for select inputs; bare strings/numbers are accepted and coerced, but an option needs a non-empty `value`. |
|
||||
| `oauth` | `{ initPath, callbackPath }` for OAuth flows. |
|
||||
|
||||
**Page nav:** the host builds a page plugin's nav entry from the top-level `name`
|
||||
@@ -1362,12 +1373,19 @@ trek-plugin release [dir] --repo owner/name --tag vX.Y.Z [--sign] [--merge entry
|
||||
|
||||
# Fork the registry, write registry/plugins/<id>.json, open the PR. Needs `gh`.
|
||||
trek-plugin submit [dir] --repo owner/name --tag vX.Y.Z [--registry o/n] [--draft]
|
||||
|
||||
# Delete a stranded release + remote tag + local tag in one go. Checks the published
|
||||
# registry index first and REFUSES a version that is actually published (immutable);
|
||||
# --yes consents when the index can't be reached.
|
||||
trek-plugin unrelease vX.Y.Z [dir] --repo owner/name [--yes]
|
||||
```
|
||||
|
||||
`publish` chains those last steps in the order that matters: **check → pack → release →
|
||||
preflight → submit**. The local gates run *before* anything is tagged or released,
|
||||
because a GitHub release is effectively immutable — the registry pins its sha256. See
|
||||
[[Publishing a Plugin|Plugin-Publishing]].
|
||||
because a GitHub release is effectively immutable — the registry pins its sha256 —
|
||||
and a failure *after* the release is cut rolls back everything that run created (the
|
||||
release, both tags), so the same tag is free to re-run against (`--keep-release`
|
||||
opts out). See [[Publishing a Plugin|Plugin-Publishing]].
|
||||
|
||||
## Registry & publishing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user