// Reference plugin: a worked example of the contract — a list page that fetches upstream // data, a CSRF-guarded form that forwards a write upstream, and permission-gated nav. Copy this // folder, rename it, point it at your own backend. Full contract: README.md → Building plugins. import { definePlugin } from "@plainpages/plugin-api"; import { createShift, createUpstream, listShifts, newShiftForm, overview, READ, SCHEDULING_PATH, SHIFTS_PATH, WRITE } from "./shifts.ts"; // The upstream this plugin reads/writes — a stand-in for your real backend (the plugin is // stateless). Its URL is a declared setting, so it is resolved and validated before onBoot hands it // over — which is after this manifest is built, hence the getter. let upstreamUrl = ""; const upstream = createUpstream(() => upstreamUrl); export default definePlugin({ apiVersion: "0.2.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. hooks: { onBoot: ({ settings }) => { upstreamUrl = settings.upstream; } }, // Merged into the global menu + filtered per user. Labels are keys in this plugin's own catalog // (i18n/.ts) — a plain string works too, it just isn't translated. "Overview" is `public`, so the "Scheduling" // header shows for everyone (even signed out); "Shifts" needs `scheduling:read`, so the gated data // stays hidden until a reader signs in (a plugin may make a page + its menu option public). nav: [{ children: [ { href: SCHEDULING_PATH, id: "scheduling:overview", label: "scheduling.nav.overview", public: true }, { href: SHIFTS_PATH, id: "scheduling:shifts", label: "scheduling.nav.shifts", permission: READ }, ], icon: "i-cal", id: "scheduling", label: "scheduling.nav.section", }], // Roles this plugin introduces (docs + Keto seeding). Namespaced `:`. permissions: [ { description: "View shifts", name: READ }, { description: "Create and edit shifts", name: WRITE }, ], // Mounted under /scheduling; `permission` gates before the handler runs. The overview is `public` // (anyone may reach /scheduling, signed in or not); the rest need a permission. routes: [ { handler: overview(), method: "GET", path: "/", public: true }, { handler: listShifts(upstream), method: "GET", path: "/shifts", permission: READ }, { handler: newShiftForm(), method: "GET", path: "/shifts/new", permission: WRITE }, { handler: createShift(upstream), method: "POST", path: "/shifts", permission: WRITE }, ], // Operator-supplied config: one PLUGIN_SETTING_SCHEDULING_UPSTREAM variable, validated as a URL at // boot. The default points at the mock backend the dev compose runs (examples/shifts-upstream). settings: [ { default: "http://shifts-upstream:4000", description: "Base URL of the backend this plugin reads shifts from and writes them to", key: "upstream", type: "url", }, ], });