From 54956fe627e99c13611a44802091846bed647a47 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 3 Sep 2026 17:58:09 +0200 Subject: [PATCH] Release the gate change as 0.3.0, and start a changelog --- CHANGELOG.md | 56 ++++++++++++++++++++++ README.md | 12 +++-- examples/plugins/admin/plugin.ts | 2 +- examples/plugins/scheduling/plugin.ts | 2 +- release-tooling/contract-version.test.ts | 2 +- release-tooling/dockerhub-overview.md.tmpl | 2 +- src/plugin-host/plugin.ts | 2 +- views/index.ejs | 2 +- 8 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9830c88 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +The release version **is** the plugin contract version (`HOST_API_VERSION`), so a minor is a +contract break: a plugin's `apiVersion` must match the host's `major.minor` or discovery refuses it +at boot. Entries start at 0.3.0. + +## 0.3.0 + +**Breaking.** Set `apiVersion: "0.3.0"`, and name a gate on every route and nav node. + +### A session is a gate of its own + +`session: true` takes any signed-in user, with no grant to hold — for a page whose data is the +visitor's own (their upstream account, their own tokens), where there is no distinction a permission +could name. An anonymous visitor is bounced to `/login` with the page as `return_to`, exactly as a +permission gate does. + +Every route and nav node now names **exactly one** of `public: true`, `session: true` or +`permission: ":"`, and a gate is spelled `true`: + +- Naming **none** is refused. It used to mean public, so a forgotten gate published a page; it now + fails the boot instead. +- Naming **two** is refused, as before. +- Spelling one anything but `true` is refused — `public: false` and `session: "yes"` both set no gate + while reading as if they set one. + +A section header gates nothing itself, so it takes `public: true` and lets each child decide; the +host still drops a header whose children all filtered out. + +`Gate` is exported from `@plainpages/plugin-api`, and `Route` and `NavNode` extend it. + +### Filter bars take a multi-select + +The `filter-bar` partial gains a `multiselect` control — the same checkboxes on the same query +parameter as `chips`, but behind a button once the list is too long to lay on the bar. Config is +`{ name, legend?, note?, value?, options }`, and the panel says what a capped list left out. + +### Fixed + +- An identity carrying no email no longer yields a session at all. Login used to mint a JWT for one, + which every later request then rejected as anonymous — leaving the browser holding a dead cookie + and no way to tell why. + +### Dependencies + +- Node 24.20.0. + +### Upgrading a plugin + +1. Set `apiVersion: "0.3.0"`. +2. Give every route and nav node a gate. Anything that relied on omitting one was public — say + `public: true` outright. + +A page that scopes rows to the signed-in visitor should join on `ctx.user.id`. An email address is +user-changeable and can be reassigned to someone else, who would then inherit the previous holder's +rows. The reference plugin's new `/scheduling/mine` page shows the shape. diff --git a/README.md b/README.md index 9a30aba..52ca628 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ folder under `plugins/` goes live after a restart. Create `plugins/hello/plugin. import { definePlugin } from "@plainpages/plugin-api"; export default definePlugin({ - apiVersion: "0.4.0", + apiVersion: "0.3.0", nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }], routes: [ { method: "GET", path: "/", public: true, handler: () => ({ html: "

Hello from my plugin

" }) }, @@ -350,7 +350,7 @@ import { definePlugin } from "@plainpages/plugin-api"; import { listThings, createThings } from "./handlers.ts"; export default definePlugin({ - apiVersion: "0.4.0", // semver string of the host contract this plugin was built against (see Versioning) + apiVersion: "0.3.0", // semver string of the host contract this plugin was built against (see Versioning) // Nav fragment, merged into the global menu and gate-filtered per user. // `icon` is a Lucide icon by its sprite id (src/ui/icons.ts). @@ -471,7 +471,7 @@ import { definePlugin } from "@plainpages/plugin-api"; import { landing, board } from "./pages.ts"; export default definePlugin({ - apiVersion: "0.4.0", + apiVersion: "0.3.0", home: landing, // owns "/" — the public front page dashboard: board, // owns "/dashboard" — the post-login app home }); @@ -626,6 +626,7 @@ provider/consumer semantics in `checkApiVersion`: The plugin pins one exact version (no ranges, per the project's pinning rules); the *host* supplies the compatibility. One digit carries the whole release, so a **minor** means either the plugin contract changed or a dependency moved far enough to warrant one. +[`CHANGELOG.md`](CHANGELOG.md) is what a minor sends you to: what broke, and what to change. ### Conflict rules @@ -756,7 +757,7 @@ camel humps both becoming underscores — so `upstream` on the `scheduling` plug ```ts export default definePlugin({ - apiVersion: "0.4.0", + apiVersion: "0.3.0", settings: [ { key: "upstream", type: "url", required: true, description: "Base URL of the backend" }, { key: "pageSize", type: "number", default: 25 }, @@ -810,7 +811,7 @@ import { definePlugin } from "@plainpages/plugin-api"; let sql: ReturnType; export default definePlugin({ - apiVersion: "0.4.0", + apiVersion: "0.3.0", storage: true, hooks: { onBoot: async (boot) => { @@ -1730,6 +1731,7 @@ e2e-tests/ Playwright specs + their Dockerfile and compose.{visual,aut release-tooling/ Everything the release runs: next-version (the bump math), contract-version (the HOST_API_VERSION↔tag gate), dockerhub-overview (+ its .md.tmpl) registry-cleanup/ Nightly image pruning — the Gitea client plus what survives (select-versions.ts) +CHANGELOG.md What changed per release, and how to upgrade a plugin across a minor ci.sh The full gate: typecheck → unit tests → every E2E suite on a fresh stack .gitea/workflows/ Gitea Actions — see CI/CD ``` diff --git a/examples/plugins/admin/plugin.ts b/examples/plugins/admin/plugin.ts index 6a407c0..d5ce0c6 100644 --- a/examples/plugins/admin/plugin.ts +++ b/examples/plugins/admin/plugin.ts @@ -28,7 +28,7 @@ const clients = on("oauth2-clients"); const pluginSettings = on("plugin-settings"); export default definePlugin({ - apiVersion: "0.4.0", // the host contract this was built against — a literal, never HOST_API_VERSION + apiVersion: "0.3.0", // the host contract this was built against — a literal, never HOST_API_VERSION nav: [ADMIN_NAV], diff --git a/examples/plugins/scheduling/plugin.ts b/examples/plugins/scheduling/plugin.ts index 215a24d..cb01632 100644 --- a/examples/plugins/scheduling/plugin.ts +++ b/examples/plugins/scheduling/plugin.ts @@ -12,7 +12,7 @@ let upstreamUrl = ""; const upstream = createUpstream(() => upstreamUrl); export default definePlugin({ - apiVersion: "0.4.0", // the host contract this was built against — a literal, never HOST_API_VERSION + apiVersion: "0.3.0", // the host contract this was built against — a literal, never HOST_API_VERSION // onBoot runs after discovery, before the server listens — where a plugin receives its resolved // settings. A malformed URL already failed the boot by then; the host validated the declared type. diff --git a/release-tooling/contract-version.test.ts b/release-tooling/contract-version.test.ts index df5b356..65fec87 100644 --- a/release-tooling/contract-version.test.ts +++ b/release-tooling/contract-version.test.ts @@ -12,7 +12,7 @@ test("readHostApiVersion pulls the constant out of the real source, and returns test("bumping HOST_API_VERSION is a deliberate act, so pin the shipped value", () => { // Not a substitute for the release gate — this test cannot see a tag. It is the tripwire that // makes an accidental edit fail here rather than at release time. - assert.equal(readHostApiVersion(readFileSync("src/plugin-host/plugin.ts", "utf8")), "0.4.0"); + assert.equal(readHostApiVersion(readFileSync("src/plugin-host/plugin.ts", "utf8")), "0.3.0"); }); test("every author-facing apiVersion sample matches the shipped contract", () => { diff --git a/release-tooling/dockerhub-overview.md.tmpl b/release-tooling/dockerhub-overview.md.tmpl index bd4a988..988dac5 100644 --- a/release-tooling/dockerhub-overview.md.tmpl +++ b/release-tooling/dockerhub-overview.md.tmpl @@ -182,7 +182,7 @@ into the app. Create `plugins/hello/plugin.ts`: import { definePlugin } from "@plainpages/plugin-api"; export default definePlugin({ - apiVersion: "0.4.0", + apiVersion: "0.3.0", nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }], routes: [ { method: "GET", path: "/", public: true, handler: () => ({ html: "

Hello from my plugin

" }) }, diff --git a/src/plugin-host/plugin.ts b/src/plugin-host/plugin.ts index 4c4716f..cb5fdb6 100644 --- a/src/plugin-host/plugin.ts +++ b/src/plugin-host/plugin.ts @@ -11,7 +11,7 @@ import { envName, type SettingDecl, type SettingsOf } from "./settings.ts"; import type { StorageCredentials } from "./storage.ts"; // The Plainpages release this contract ships in — see README → Contract versioning. -export const HOST_API_VERSION = "0.4.0"; +export const HOST_API_VERSION = "0.3.0"; export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT"; diff --git a/views/index.ejs b/views/index.ejs index 29a9dee..988cd16 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -14,7 +14,7 @@

${t("dashboard.starter.intro")}

${t("dashboard.starter.replace")}

export default definePlugin({
-  apiVersion: "0.4.0",
+  apiVersion: "0.3.0",
   // view names plugins/<id>/views/<view>.ejs, rendered in this same shell
   dashboard: (ctx) => ({ view: "dashboard", data: { /* … */ } }),
 });