Move reference plugin to examples/scheduling-plugin; plugins/ ships empty as a drop-in mount point, e2e bind-mounts the example

This commit is contained in:
2026-06-27 00:02:26 +02:00
parent d8cf257940
commit fe97c3854a
19 changed files with 44 additions and 21 deletions
+4
View File
@@ -5,3 +5,7 @@ node_modules
# Playwright E2E outputs (screenshots, html report, traces)
e2e-tests/artifacts/
# plugins/ is a drop-in mount point, not committed code — keep it empty (see examples/ for the reference)
/plugins/*
!/plugins/.gitkeep
+15 -11
View File
@@ -53,7 +53,7 @@ is the plugin id *and* the mount path) and "Hello" is in the menu. That's the wh
From here, render real pages against the app shell and fetch upstream data — see
[Building plugins](#building-plugins) and the runnable reference in
[`plugins/scheduling/`](plugins/scheduling/).
[`examples/scheduling-plugin/`](examples/scheduling-plugin/).
## Contents
@@ -205,11 +205,15 @@ override (see [The menu system](#the-menu-system)).
This is the **authoritative reference** for the plugin API — the product's main surface. The
contract is **TypeScript** (`src/plugin-host/plugin.ts`), so the types there are the single
source of truth; the sections below explain them, the guarantees around them, and the rules
the host enforces. A complete, runnable example ships in
**[`plugins/scheduling/`](plugins/scheduling/)** — a public overview page, a permission-gated
list page fetching upstream data (it points `SCHEDULING_UPSTREAM` at its backend; the dev
compose ships a tiny mock, `examples/shifts-upstream/`), a CSRF-guarded form forwarding writes
upstream, and a mix of public + role-gated nav. Copy it and adapt.
the host enforces. A complete, runnable example lives in
**[`examples/scheduling-plugin/`](examples/scheduling-plugin/)** — a public overview page, a
permission-gated list page fetching upstream data (it points `SCHEDULING_UPSTREAM` at its backend;
the dev compose ships a tiny mock, `examples/shifts-upstream/`), a CSRF-guarded form forwarding
writes upstream, and a mix of public + role-gated nav. It is **not** pre-installed — `plugins/`
ships empty so you mount your own; the E2E suites bind-mount this example onto
`/app/plugins/scheduling` to exercise it. To run it in dev, copy it in
(`cp -r examples/scheduling-plugin plugins/scheduling`, then restart) — the dev compose already
points `SCHEDULING_UPSTREAM` at its mock backend. Copy it to `plugins/<id>/` and adapt.
**Design stance.** The audience is experienced developers. The API optimises for being
**powerful, predictable, and overloadable** — a plugin can take over as much of a page as it
@@ -439,11 +443,11 @@ role-filtered, and current-marked for this request (the gated **Dashboard** link
anonymous visitor). `chrome.signInHref` is where the shell's anonymous **Sign in** link points — the
current page baked in as `return_to`. Map each `chrome.*` to the matching `partials/shell` local —
`brand`, `csrfToken`, `nav` (the rendered nav-tree), `signInHref`, `theme`, `user` — exactly as the
reference `plugins/scheduling/views/overview.ejs` does; a value you forget simply falls back to its
reference `examples/scheduling-plugin/views/overview.ejs` does; a value you forget simply falls back to its
shell default (e.g. a bare `/login`), it does not error. **`ctx.verifyCsrf(submitted)`** guards a
state-changing form: render `chrome.csrfToken` in a hidden `_csrf` field, then on POST read your own
body and `if (!ctx.verifyCsrf(form.get("_csrf"))) throw new GuardError(403, …)`. The host owns the
secret and sets the cookie; the plugin never touches it. (See the reference: `plugins/scheduling/`.)
secret and sets the cookie; the plugin never touches it. (See the reference: `examples/scheduling-plugin/`.)
The same shell renders **every** page (the dashboard, the admin screens, your plugin pages, and the
login/registration/front pages), so the menu looks identical signed in or out — it just role-filters.
@@ -617,7 +621,7 @@ reproducible; mount a volume only to add plugins to an already-built image.
### Local dev & test story
A plugin is a normal folder of TypeScript, so an author tests it the same way the core is tested
— everything in Docker, no host tooling. The shipped reference (`plugins/scheduling/`) is the
— everything in Docker, no host tooling. The reference example (`examples/scheduling-plugin/`) is the
worked example: thin handlers bound to an injectable upstream client, unit-tested in
`shifts.test.ts` with a mocked `fetch` and a hand-built `ctx` (no host).
@@ -1276,8 +1280,8 @@ views/ Core EJS templates, all in the one app shell: home (public
public/ Static assets under /public/ (css/styles.css + auth.css, favicon, robots.txt)
config/menu.ts Central menu override + branding (optional; defaults apply if absent)
ory/ Ory service config (kratos/: identity schema, kratos.yml, oidc/ SSO claims mapper, tokenizer/ session→JWT claims mapper + dev signing JWKS; keto/: keto.yml + namespaces.keto.ts OPL — role/group/resource; hydra/hydra.yml: OAuth2 issuer + login/consent URLs → /oauth2/*) + storage init (postgres/init/init.sql: one DB per service)
plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in). Ships scheduling/ — the reference plugin (list/form over an upstream + permission-gated nav) you copy
examples/ Non-app helpers; shifts-upstream/ is the dev mock backend the reference plugin reads/writes (stand-in for your real service)
plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in). Ships empty (.gitkeep, git-ignored otherwise) — mount your own; the E2E suites bind-mount the reference example onto /app/plugins/scheduling
examples/ Non-app helpers: scheduling-plugin/ is the reference plugin (list/form over an upstream + permission-gated nav) you copy into plugins/; shifts-upstream/ is the dev mock backend it reads/writes (stand-in for your real service)
e2e-tests/ Playwright E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login + consent) + full-flow.spec (browser UI: password/SSO login, menu-by-role, admin CRUD, plugin page, logout) + devstack-login.spec (regression: login works from the banner's localhost URL and 127.0.0.1 is canonicalised, on the plain `docker compose up` topology); proxy.ts (same-origin gateway) + mock-oidc.ts (mock SSO provider) back full-flow. e2e-tests/Dockerfile + e2e-tests/compose.{visual,auth,oauth,full,devstack}.yml run them
ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash ci.sh`)
```
+5 -4
View File
@@ -15,14 +15,15 @@ services:
LOG_LEVEL: "debug" # verbose by default while developing (base defaults to info)
REQUIRE_SECURE_SECRETS: "false"
SECURE_COOKIES: "false" # dev serves http — Secure cookies wouldn't be sent
SCHEDULING_UPSTREAM: "http://shifts-upstream:4000" # reference plugin → the dev mock backend
SCHEDULING_UPSTREAM: "http://shifts-upstream:4000" # backs the reference plugin once you copy it into plugins/
volumes:
- .:/app
- /app/node_modules
# Dev mock backend for the reference plugin (plugins/scheduling). A stand-in for the customer's
# real scheduling service — stdlib-only, in-memory, no auth. Prod points SCHEDULING_UPSTREAM at
# the real backend instead. Uses the pinned app image so there's nothing extra to build/pull.
# Mock backend ready for the reference plugin (examples/scheduling-plugin): plugins/ ships empty, so
# the plugin is opt-in — `cp -r examples/scheduling-plugin plugins/scheduling`, restart, and this
# backs it (SCHEDULING_UPSTREAM above points here). Stand-in for the customer's real service —
# stdlib-only, in-memory, no auth. Prod points SCHEDULING_UPSTREAM at the real backend instead.
shifts-upstream:
image: node:24.16.0-alpine3.24
command: node /srv/server.ts
+10
View File
@@ -28,6 +28,16 @@ services:
interval: 2s
timeout: 4s
retries: 30
# plugins/ is empty in the image; bind the reference example in as the `scheduling` plugin so the
# browser flow can open its gated /scheduling/shifts page.
volumes:
- ./examples/scheduling-plugin:/app/plugins/scheduling:ro
# bootstrap grants the demo admin every discovered plugin's permission tokens, so it needs the
# reference plugin present too — else the admin lacks scheduling:read/write and the gated page 403s.
bootstrap:
volumes:
- ./examples/scheduling-plugin:/app/plugins/scheduling:ro
# Browser-facing URLs (base_url, every ui_url, the after-login redirect) move to the gateway host.
# `--dev`: the browser hits the gateway over http, but Kratos marks cookies Secure for a
+4
View File
@@ -20,6 +20,10 @@ services:
interval: 2s
timeout: 4s
retries: 15
# plugins/ is empty in the image; bind the reference example in as the `scheduling` plugin so the
# nav-gating spec has a drop-in plugin to assert against.
volumes:
- ./examples/scheduling-plugin:/app/plugins/scheduling:ro
e2e:
build:
+2 -2
View File
@@ -1,5 +1,5 @@
// Dev-only mock upstream for the reference plugin (plugins/scheduling) — a stand-in for the
// customer's real backend so `docker compose up` shows the plugin working out of the box. NOT part
// Dev-only mock upstream for the reference plugin (examples/scheduling-plugin) — a stand-in for the
// customer's real backend, ready for when you copy the reference plugin into plugins/. NOT part
// of the app: stdlib only, in-memory (state resets on restart), no auth. Point SCHEDULING_UPSTREAM
// at your real service in production.
//
+1 -1
View File
@@ -11,7 +11,7 @@
"dev": "node --watch src/server.ts",
"gen-jwks": "node src/auth/gen-jwks.ts",
"typecheck": "tsc --noEmit",
"test": "node --test \"src/**/*.test.ts\" \"plugins/**/*.test.ts\""
"test": "node --test \"src/**/*.test.ts\" \"plugins/**/*.test.ts\" \"examples/**/*.test.ts\""
},
"dependencies": {
"@larvit/log": "2.3.0",
View File
+2 -2
View File
@@ -2,6 +2,6 @@ For each todo item, interview the user extensively to deeply understand the scop
- [x] Organize the files in src in to folders so it is easier to understand the structure of the code.
- [x] Move docs/plugin-contract.md into README.md and remove the docs folder.
- [ ] The plugins/scheduling is an example and shouldn't be committed to the plugins directory since that should be empty to be able to be mounted in via docker or other means for the users/develoeprs using this application/framework. Put it in the examples folder instead.
- [ ] Build and publish docker image as CI/CD. Ma
- [x] The plugins/scheduling is an example and shouldn't be committed to the plugins directory since that should be empty to be able to be mounted in via docker or other means for the users/develoeprs using this application/framework. Put it in the examples folder instead.
- [ ] Build and publish docker image as CI/CD.
- [ ] Add i18n support.
+1 -1
View File
@@ -24,5 +24,5 @@
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["config", "plugins", "src"]
"include": ["config", "examples/scheduling-plugin", "plugins", "src"]
}