20 KiB
20 KiB
Plainpages — implementation TODO
Build order is top → bottom; each phase is roughly independent and testable. Conventions: write tests first (node --test for units, Playwright for E2E), tear down test containers after runs, keep deps minimal, pin all versions, run everything via Docker.
North-star / MVP. Done = a developer can clone, run one command, get a working register/login, and start hacking on their own plugin — no manual key generation, no hand-edited Ory config, no DB setup. Everything below serves that; the one-command bootstrap (§3) and the example plugin (§7) are what make the MVP real. Hydra/SSO are explicitly post-MVP.
0. Housekeeping / primitives
- Decide JWT verify approach:
node:crypto(RS256/ES256 viacreatePublicKey({format:"jwk"})) vs addjose— justify if adding. →node:crypto(no new dep);src/jwt.tsverifies JWS signatures. - Cookie helpers: parse
Cookieheader, buildSet-Cookie(HttpOnly, Secure, SameSite). →src/cookie.ts(parseCookies/serializeCookie); stdlib-only, injection/pollution-safe. - Request context type threaded to handlers:
{ req, res, url, params, query, user|null, roles }. →src/context.ts(RequestContext+buildContext);rolesmirroruser.roles, the §2 router/§4 JWT middleware supplyparams/user. - Error templates: add 403 + 500 (404 exists). →
views/403.ejs+views/500.ejs; 500 wired intoapp.tserror handler (HTML, plain-text fallback). - Config/env loader: Ory endpoints, cookie/CSRF secret, JWKS location, ports. →
src/config.ts(loadConfig); validated at boot, dev defaults for clean-clone, prod requires real secrets; wired intoserver.ts. - Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues. → Both: no bugs/security issues. Addressed: wired
buildContextintoapp.ts; graceful SIGTERM/SIGINT shutdown; EJS template caching in prod. Deferredcore//shell/split (premature for an 8-file scaffold; revisit at §2/§4). - Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff. → Tightened comments across
src/*.ts, Dockerfile, and trimmed verbose/duplicated prose in README; tests + typecheck green. - Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us. → Merged related cases across jwt/cookie/app/context/config tests (59 → 42), every assertion preserved; typecheck + tests green.
0.1 Extra input from human
- Remove all usage of NODE_ENV - add a new core principle to the project that the app should at all times be unaware of what environment it is running in. Configuration should be explicit, like "disable email" or "cache templates". → Dropped NODE_ENV everywhere; added environment-agnostic principle (AGENTS.md §4 + README). Behaviour is now explicit toggles:
CACHE_TEMPLATES,REQUIRE_SECURE_SECRETS(parsed/validated inconfig.ts, wired viaserver.ts); compose files set them per deployment.app.tsno longer readsprocess.env.
1. Building blocks — extract from html-css-foundation/ (no Ory needed; render mock data)
- Move
styles.css+auth.cssintopublic/css/; remove existingstyle.css. →git mvfromhtml-css-foundation/intopublic/css/; dropped the placeholderstyle.css; views + tests now referencestyles.css; foundation mockups repointed to../public/css/. - Lucide icon sprite from
lucide-static(dep added) →views/partials/icons.ejs; serve/inline only the icons used. →src/icons.ts(id→lucide map +buildIconSprite) generates a hidden<symbol>sprite of the 31 icons the mockups reference, paths sourced from pinned lucide-static;icons.test.tsguards provenance + only-used. Stale image rebuilt (lucide-static was missing). Wiring into the app shell is the next item. - App-shell partial (sidebar + topbar + content slot). →
views/partials/shell.ejs: full document wrapping.app→ sidebar (brand +navslot + theme/profile footer) ·.scrim·.content(.topbar+bodyslot); reuses the mockup's classes (styled bystyles.css), inlines the icon sprite. Slotsnav/actions/bodyare HTML locals,title/brand/user/breadcrumbstext; defaults render standalone.shell.test.tscovers landmarks, slots, escaping, defaults. Not yet routed (that's "replace placeholder index"). - Nav-tree partial — recursive, header/leaf × clickable/static, counts,
aria-current. →views/partials/nav-tree.ejs: data-driven, self-including. Node{ label, href?, icon?, count?, current?, open?, children? }; header (children →.nav-disctoggle + sibling.nav-children) vs leaf (spacer), clickable (<a>) vs static (<span>), orthogonal. Renders into the shell'snavslot.nav-tree.test.tscovers the full matrix + counts/icons/aria-current/escaping/empty. - Filter-bar partial — GET form (search, segmented, selects, chips, daterange, applied pills). →
views/partials/filter-bar.ejs: data-driven<form method="get">(server-side, zero-JS).rows: Control[][],type ∈ search|segmented|select|chips|daterange|spacer, each reflecting current value (checked/selected); plus appliedpills(+ remove links, Clear all) and Reset/Apply actions. Columns/“more filters” menus deferred to the menu/popover item.filter-bar.test.tscovers every type + value reflection + pills + defaults. - Data-table partial — sortable headers, row-select, badges, kebab row actions. →
views/partials/data-table.ejs: data-driven, zero-JS.columns({ label, sortable, sort, href, className }) render sort as<a class="th-sort">+aria-sort(links, not the mockup's inert buttons);selectable/actionstoggle the check/kebab columns.rowscarry typedcells(string | text+class | user/avatar | badge tone | raw html) + kebabactions(link or danger button, separators).data-table.test.tscovers the matrix + minimal/empty defaults. - Pagination partial — rows-per-page + page numbers, query-param driven. →
views/partials/pagination.ejs: data-driven, zero-JS.summary {from,to,total}, rows-per-page GET<form>(select + submit,hidden[]carries list state),pages: {label,href?,current?,ellipsis?}[](links; current/ellipsis inert),prev/next(href ⇒ link, omit ⇒ disabled). Reuses the mockup's.pagerCSS, no changes.pagination.test.tscovers the matrix + value reflection + empty defaults. - Form-field partials (input/label/hint/error) + auth-card partial. →
views/partials/field.ejs: data-driven.field— label (+ inlinelink/Optional), optional icon input (has-ico),hint, server-drivenerror(string | {text} | {html}) wiringaria-invalid+aria-describedby; added one CSS rule.field.has-error .field-error{display:flex}so a rendered field shows its own error.views/partials/auth-card.ejs: the<form class="auth-card">shell — head (back/title/sub), optionalssoproviders (text logo or icon, link or button) + divider,bodyslot (fields + submit),altfooter.field.test.ts/auth-card.test.tscover the matrix + escaping + defaults. - Menu/popover + theme-switch partials (pure CSS
details/summary). →views/partials/menu.ejs: data-driven<details>popover —trigger(icon/text/raw-html,class:""⇒ bare kebab),align/uppositioning,width;items= head · sep · link/button (icon, danger) · check-group(the columns/“more filters” menus filter-bar deferred here).views/partials/theme-switch.ejs: Light/Auto/Dark radiogroup with the fixedtheme-light/auto/darkidsstyles.csskeys its:has()swaps off. Added.menu-pop.up(replaces the mockup's inline up-positioning);shell.ejsnow reuses both partials.menu.test.ts/theme-switch.test.tscover the matrix + escaping + defaults. - Helper
composeNav(fragments, override, roles)→ merged, permission-filtered tree. →src/nav.ts: pure, I/O-free. Flattens plugin fragments, applies the central override (rename → group → order → hide, all keyed by nodeid), then role-filters — a node shows iff it has nopermissionorrolesincludes it; a gated header drops its whole subtree, an emptied pure header is dropped. Emits clean nodes (noid/permission, absent fields omitted) ready fornav-tree.ejs. Filter runs last so everything above is per-deployment.NavNode/NavOverride/NavGroupSpectypes exported;nav.test.tscovers merge/filter/empties/override matrix. - Helper
parseListQuery(url)→{ q, filters, sort, page, pageSize }. - Helper
paginate(total, page, pageSize)→ page model. - Replace placeholder
indexwith the app-shell dashboard. - Check the full system in Playwright and make screenshots and compare to the static original design in html-css-foundation to make sure we're showing the correct graphics.
- Go over all HTML and CSS and make adjust it to be as sematic as we can, css classes, ids html elements and all, then add semantic DOM as a priority in this project.
- Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
1.1 Extra input from human
- Add to principles that we should have full E2E coverage in the Playwright tests - make sure they can run in parallel to get up some speed.
2. Plugin host
- Specify the plugin contract (big job, do first — it's the product's main API surface). Write it down as the authoritative reference: the full manifest shape; the
RequestContexthanded to handlers and what's guaranteed stable; contract versioning (aapiVersion/engines-style field so a plugin declares the host it targets, and the host refuses or warns on mismatch); conflict rules (two plugins claiming the samebasePath, nav slot, orpermissionname → defined, loud resolution, not last-write-wins); the local dev/test story (how an author runs + tests one plugin in isolation against the host). Audience is experienced devs: optimise for a powerful, predictable, clearly-documented API. Crash-isolation (a bad plugin can't take down the host) is a nice-to-have, not a blocker — fail loud at boot/discovery over sandboxing at runtime. - Discovery: scan
plugins/, import eachplugin.tsdefault export, validate. - Router: match method+path under
basePath, resolve path params, run permission gate, call handler with context. - Per-plugin view resolver (
plugins/<id>/views/*.ejs). - Per-plugin static serving:
plugins/<id>/public/→/public/<id>/. config/menu.tscentral override: reorder/rename/hide/group + branding (app name, logo, default theme).- Wire branding into the app shell.
- Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
3. Ory stack — compose + config
postgresservice (pinned tag); separate DB/schema per Kratos/Keto/Hydra.kratosservice (pinned) +migrate; identity schema (traits: email, name).- Kratos self-service flows (login, registration, recovery, verification, settings) → return URLs at our themed pages.
- Kratos OIDC/SSO providers (Google/Microsoft/SAML) config (secrets via env). None enabled by default — a clean clone runs password-only; a provider activates purely by supplying its env creds.
- Kratos session settings (cookie name, lifespan, sliding refresh).
- Kratos tokenizer template
plainpages: claims{ sub, email, roles },ttl ≈ 10m,jwks_urlsigner,claims_mapper_url(Jsonnet readingmetadata_admin.roles). - Generate + mount the JWT signing JWKS; document key rotation.
ketoservice (pinned) +migrate; namespaces in OPL (role,group, resource permissions).hydraservice (pinned) +migrate; issuer + login/consent URLs → our app.- Split dev (
compose.override.yml) vs prod (compose.yml) wiring; health checks +depends_onordering. - One-command bootstrap (the MVP bar):
docker compose upbrings up web + all Ory services + Postgres with zero manual prep. Commit working default Ory configs; auto-run migrations on first boot; auto-generate the JWKS signing key if absent; seed an admin identity + its Keto roles + a demo password (admin/admin) idempotently. Land anOPL/namespace bootstrap so Keto answers checks out of the box. - First-run banner / log line printing the login URL + seeded admin creds, with a clear "change these before production" warning.
- Document the only things that can't be auto-generated: third-party SSO provider client id/secret (optional — password login works without them) and production secrets (real cookie/CSRF secret + signing key, supplied via env, replacing the dev throwaways). Everything else must work from a clean clone.
- Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
4. Auth — identity, session JWT, guards
- Kratos public client (fetch): init/get/submit flows,
whoami,whoami?tokenize_as=plainpages. - Kratos admin client (fetch): identity CRUD +
metadata_adminupdate. - Keto client (fetch):
check, list/expand relations, write/delete tuples. - Render Kratos flows: fetch flow → render fields against our themed pages → POST to
flow.ui.action(Kratos handles its CSRF), map field errors/messages. - SSO buttons → Kratos OIDC flows. Render per configured provider only: derive the list from Kratos' enabled OIDC providers (no creds ⇒ no button); hide the whole SSO section when none are configured. No code change needed to add/remove a provider — config only.
- Login completion: read roles from Keto → write
metadata_adminprojection → tokenize → set JWT cookie. - JWT middleware: verify signature via cached JWKS, validate
exp/iss/aud(+clock skew), build context (user, roles). - JWKS fetch + cache + rotation handling.
- Guards:
requireSession(validate JWT),can(role)(claim, in-process),check(relation, object)(live Keto). - Session re-mint on TTL expiry (re-read roles from Keto).
- Logout: revoke Kratos session + clear cookie.
- Secure cookie flags; CSRF for our own POST forms.
- Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
5. Built-in admin screens (writes go only to Keto/Kratos)
- Users: list (Kratos identities) with filter/sort/pagination; create/edit/deactivate/delete; trigger recovery.
- Groups: Keto subject sets — list/create/delete + membership management.
- Roles & permissions: Keto relations — assign roles to users/groups; "effective access" view via Keto expand.
- Wire into the menu (admin section, permission-gated).
- Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
6. Hydra — OAuth2/OIDC provider (can ship after the rest)
- Login-challenge handler: authenticate via Kratos session, accept/reject.
- Consent-challenge handler: show / auto-accept first-party, grant scopes, accept/reject.
- OAuth2 client registration (admin UI or CLI).
- Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
7. Example plugin (reference)
- Reference plugin (e.g. people directory or scheduling): list page fetching upstream data, a form that forwards writes upstream, permission-gated nav.
- Verify the full plugin contract end-to-end against the README.
- Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
8. Testing & CI
- node --test units across helpers / router / nav / auth (tests-first throughout).
- Playwright full E2E: login (password + mocked SSO), menu filtering by role, users/groups/permissions CRUD, a plugin page, logout.
- E2E harness: bring up the full compose stack, seed Keto roles + a test identity, tear down after.
- Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
9. Production, security, ops
compose.ymlprod: Ory + Postgres, secrets via env, no source mount.- Security headers; secure/HttpOnly/SameSite cookies; CSRF; clock-skew tolerance.
- Optional revocation denylist for instant role/session revoke.
- Structured logging / basic observability. use @larvit/log for OTLP compability - but add subtasks and stuff for supporting incoming trace id etc from a reverse-proxy etc.
- JWT signing-key rotation runbook.
- Refresh README
Layout+ drop_(planned)_markers as pieces land. - Run the architecture and the stability reviewer agents on the whole project, not just the latest changes, and address their issues.
- Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.