Add a goal for users of the building blocks, and plan todo.md as a scored table
This commit was merged in pull request #174.
This commit is contained in:
@@ -131,32 +131,40 @@ From here, render real pages against the app shell and fetch upstream data — s
|
|||||||
forms built only from the core building blocks.
|
forms built only from the core building blocks.
|
||||||
- Counter-signal: the core grows features only one project needs.
|
- Counter-signal: the core grows features only one project needs.
|
||||||
- Why: boilerplate removed by absorbing a project's domain only moves it into the core.
|
- Why: boilerplate removed by absorbing a project's domain only moves it into the core.
|
||||||
2. **Maintain:** Plainpages solves only the parts every project shares: auth, the GUI
|
2. **Achieve:** Every building block works for the person using it: a non-technical user
|
||||||
|
gets what they meant on the first try, and can't hurt themselves by accident.
|
||||||
|
- Signal: bugs users hit in a shipped flow get fixed and stay fixed, such as a pasted
|
||||||
|
code being refused, a double click acting twice, or an admin locking themselves out.
|
||||||
|
- Counter-signal: a fix adds client-side JavaScript, or a setting every plugin has to
|
||||||
|
know about.
|
||||||
|
- Why: a fix that breaks zero-JS or simplicity moves the irritation from users to every
|
||||||
|
plugin author.
|
||||||
|
3. **Maintain:** Plainpages solves only the parts every project shares: auth, the GUI
|
||||||
building blocks (menu, tables, filters, forms) and the plugin structure. Everything
|
building blocks (menu, tables, filters, forms) and the plugin structure. Everything
|
||||||
else is a plugin.
|
else is a plugin.
|
||||||
3. **Maintain:** Every page is a server-rendered document that works with zero JavaScript.
|
4. **Maintain:** Every page is a server-rendered document that works with zero JavaScript.
|
||||||
4. **Maintain:** State lives in the URL. Only state the URL can't carry lives in the
|
5. **Maintain:** State lives in the URL. Only state the URL can't carry lives in the
|
||||||
database, never in local storage or JavaScript. A cookie carries only what must be bound
|
database, never in local storage or JavaScript. A cookie carries only what must be bound
|
||||||
to the browser: the session and its CSRF token.
|
to the browser: the session and its CSRF token.
|
||||||
5. **Maintain:** The frontend is semantic: the HTML says what each thing is, and the CSS
|
6. **Maintain:** The frontend is semantic: the HTML says what each thing is, and the CSS
|
||||||
styles meaning, not looks. That keeps every page accessible.
|
styles meaning, not looks. That keeps every page accessible.
|
||||||
6. **Achieve:** Hackable: a plugin can change anything, and Plainpages stays out of its way.
|
7. **Achieve:** Hackable: a plugin can change anything, and Plainpages stays out of its way.
|
||||||
- Signal: a plugin replaces any page, partial, menu entry or string without forking the
|
- Signal: a plugin replaces any page, partial, menu entry or string without forking the
|
||||||
host, and fewer plugins need a host change to get unblocked.
|
host, and fewer plugins need a host change to get unblocked.
|
||||||
- Counter-signal: plugins break on upgrade with no version signal, or a plugin mistake
|
- Counter-signal: plugins break on upgrade with no version signal, or a plugin mistake
|
||||||
shows up at runtime instead of at boot.
|
shows up at runtime instead of at boot.
|
||||||
- Why: freedom that breaks silently costs more than the boilerplate it saved.
|
- Why: freedom that breaks silently costs more than the boilerplate it saved.
|
||||||
7. **Achieve:** Speed: pages render fast and stay small.
|
8. **Achieve:** Speed: pages render fast and stay small.
|
||||||
- Signal: server time per page and bytes sent per page fall.
|
- Signal: server time per page and bytes sent per page fall.
|
||||||
- Counter-signal: a building block loses a feature or accessibility to save bytes.
|
- Counter-signal: a building block loses a feature or accessibility to save bytes.
|
||||||
- Why: a fast page that does less only moves the work back into each plugin.
|
- Why: a fast page that does less only moves the work back into each plugin.
|
||||||
8. **Achieve:** LLM-friendly: an LLM builds a correct plugin from the README and the
|
9. **Achieve:** LLM-friendly: an LLM builds a correct plugin from the README and the
|
||||||
examples alone.
|
examples alone.
|
||||||
- Signal: an agent given only those writes a plugin that passes discovery and its E2E
|
- Signal: an agent given only those writes a plugin that passes discovery and its E2E
|
||||||
test on the first try.
|
test on the first try.
|
||||||
- Counter-signal: the README grows to restate what the code already shows.
|
- Counter-signal: the README grows to restate what the code already shows.
|
||||||
- Why: docs that pass the test by growing cost every later reader, human or LLM.
|
- Why: docs that pass the test by growing cost every later reader, human or LLM.
|
||||||
9. **Maintain:** Every behaviour is an explicit configuration choice, the same in every
|
10. **Maintain:** Every behaviour is an explicit configuration choice, the same in every
|
||||||
environment.
|
environment.
|
||||||
|
|
||||||
## Audience
|
## Audience
|
||||||
|
|||||||
@@ -1,74 +1,184 @@
|
|||||||
# Primary todo
|
# todo
|
||||||
|
|
||||||
## Unfinnished work
|
## Scoring
|
||||||
|
|
||||||
- [ ] **Publish every release to Docker Hub, and give the Hub page its short description.** Today Hub's newest tag is 0.4.0 (2026-09-10), while git has v0.4.1–v0.4.12, and the overview still says 0.4.0. No `release.yml` run exists for the tags auto-release pushes as `renovate-bot`, so neither the images nor the overview reach Hub; find out why Gitea doesn't fire `push: tags` for them, fix it, and backfill the missing tags. The short description is empty. fejkdata already did this (`../fejkdata`, commits `51d924a` and `34c8234`), and two parts port directly: a `dockerhub/description.txt`-style one-liner, which here can ride the existing PATCH as its `description` field; and a unit test that the description opens a README sentence and every command the overview shows appears in the README. fejkdata also syncs the page on any push to `main` that changes it, with its own Read/Write/Delete token in an environment only `main` may use, so the image-push token needs no Delete scope. Decide whether that fits here, where `{{VERSION}}` must render a released tag.
|
`Score = 100 * V / (2**S + 8)`
|
||||||
- [ ] Rename the plugin "admin" to something less generic, like "auth-admin" or "users-groups-admin".
|
|
||||||
- [ ] Guard the group paths against self-lockout, or accept them explicitly. The self-revoke guard covers only your own *direct* grants on the Users screen; unticking a permission on a group you belong to, removing yourself from that group, or deleting it can all still strip your own effective access with no warning. Recorded in AGENTS.md as a known gap — the robust fix is a "last effective holder" check, which needs a reverse Keto query.
|
|
||||||
- [ ] The permission picker has no concurrency baseline, so two operators editing the same user/group silently discard each other's change. Sketch: post the rendered set as a hidden baseline; if it no longer matches Keto, re-render with "this changed while you had the page open" rather than applying.
|
|
||||||
- [ ] A grant whose plugin was uninstalled is invisible and unremovable in the GUI. `grantDiff` deliberately never revokes an undeclared name, but nothing *shows* it either — so it can't be audited or cleaned, and reinstalling that plugin silently reactivates access nobody remembers granting. Sketch: a read-only "held, but no installed plugin offers this" list with a remove action.
|
|
||||||
- [ ] A plugin may gate a route on a permission it never declares — declaring stays optional on purpose. The cost is a dead end: the picker is built from declarations only, so that route is ungrantable from the GUI with no boot error and a permanent 403 as the operator's only clue. Sketch: a discovery *warning* (not an error) naming the gated-but-undeclared permission.
|
|
||||||
- [ ] Saving permissions gives no confirmation, and a partial failure is silent. `applyGrants` loops writes then deletes with no transaction, so a Keto error midway leaves a half-applied set behind the generic error page; and a successful save is indistinguishable from "nothing changed". The `alert alert-pos` pattern the recovery-code banner uses is already available.
|
|
||||||
- [ ] In Playwright tests, try different resolutions and sizes, from BIG desktop down to tiny phone.
|
|
||||||
- [ ] Decide whether `e2e-tests/` should be typechecked. It is outside `tsconfig.include`, so the gate never checks its most logic-bearing file (`console-guard.ts`). Including it needs the DOM lib and `@playwright/test` present wherever `npm run typecheck` runs, which today is the `web` image that installs neither.
|
|
||||||
- [ ] Decide whether Playwright's `workers` should be pinned. Unset, it sizes the pool from `os.cpus()`, which reports the host's cores regardless of a container CPU quota — and with `retries: 0` a starved runner turns a slow test straight into a red gate. Fine on the current act_runner; revisit if CI ever runs constrained.
|
|
||||||
- [ ] Record the browser floor Plainpages requires, and whether the fallback is the contract or a courtesy. The stylesheet needs `:has()` (Dec 2023); the menus need the popover API (Safari 17) and CSS anchor positioning for placement (newer still, and unguarded — the `@supports` test covers popover only). An iPadOS 16 tablet, capped at Safari 16, therefore gets panels flowing inline rather than working menus. Either state a supported floor or accept the fallback for those devices; nobody has rendered that path on real hardware.
|
|
||||||
- [ ] Decide whether the profile dropdown still earns a dropdown. It holds one item, Sign out, behind a click, and its "Signed in as X" head repeats what the trigger already shows.
|
|
||||||
- [ ] Trim whitespace around the verification code — a copy+pasted code from the email fails, today as a browser `pattern` refusal rather than a Kratos rejection, now carrying a digits-only hint so that refusal isn't bare. The paste still fails. **Own session.** No zero-JS fix exists: `pattern` is rejective and cannot transform, so loosening it only lets the untrimmed value through. The real fix is a host-side proxy of the flow POST, which is feasible and half-built — `submitFlow` (`src/auth/kratos-public.ts:132`) already relays cookies and normalises a 422 `redirect_browser_to`, and has no callers outside tests. CSRF improves rather than breaks: the host already writes Kratos' cookie onto its own origin (`src/auth/routes.ts:77`), so the POST becomes same-origin, and `form-action 'self'` could finally join the CSP. Two risks: one `flow.ui.action` serves all five flows, so this rewrites the sign-in path, not just code entry; and Go's nosurf checks Referer only over https, which no test here covers. **Check first:** `kratos-admin.ts:75` returns a `recovery_link` alongside the code — if the stock courier mail carries one (unverified), a template change fixes the UX with no host code. Code entry has no E2E coverage today either way.
|
|
||||||
- [ ] Guard against the double-clicked submit, without client-side JavaScript. A non-technical user clicks a button twice when nothing happens fast enough, so a second identical POST is an expected event, not misuse — today it creates two users, mints two recovery codes, or registers two OAuth2 clients. Constraints: HTML/CSS only, and it must not break an action that is *legitimately* repeatable. Sketch: a CSS-only affordance so the second click has nothing to hit, paired with the host recognising a duplicate on the server — same session, route and payload within a short window — then logging and dropping it. Open questions: what identifies "the same submission" (a one-time token minted into each rendered form beats hashing the payload, and the CSRF plumbing already mints per-request tokens), the window length, where the record lives given the app is stateless, and how a plugin declares a route repeatable.
|
|
||||||
- [ ] Decide the caching contract for rendered pages. Responses carry `Vary: Accept-Language` but nothing sets `Cache-Control`, so a shared cache has no instruction and a signed-in page is not marked `private`. Either set the headers deliberately (public cacheable, gated `private, no-store`) or record in AGENTS.md that the reverse proxy owns this.
|
|
||||||
- [ ] Decide whether the single generic Keto `Resource` namespace should become per-domain namespaces (`Shift`, `Document`, …), as Ory's own examples model it. One global `Resource` bucket is the project's own "no catch-all names" rule applied to namespaces. A design question, not a naming one.
|
|
||||||
- [ ] Decide what `ICON_NAMES` (`src/ui/icons.ts`) actually is. `i-chart`, `i-copy`, `i-download` and `i-sliders` have no caller anywhere — so either they go, or the comment should say the palette is curated and may carry an id ahead of its first use. Not cosmetic: the sprite is inlined into every page, and the rule decides whether a future removal is routine cleanup or a plugin-facing regression.
|
|
||||||
- [ ] Decide (once) whether the CSRF token staying unbound to `sub`/session is accepted. `src/auth/csrf.ts` signs `<nonce>.<HMAC(secret, nonce)>` with no session binding, so any validly-signed token passes for any user — an attacker who can write cookies on the origin can fix a token they know. Standard for unbound signed double-submit and plausibly fine behind `SameSite=Lax` + HSTS. Accepted ⇒ record it in AGENTS.md and README → Security model; not accepted ⇒ bind the nonce to `sub`.
|
|
||||||
- [ ] Verify the documented Docker commands on macOS and fix whatever misbehaves — **macOS is a supported dev host**, but nothing here has been run on one. Two suspects, both from the `--user "$(id -u):$(id -g)"` idiom: a macOS `id -g` is `20`, which is `dialout` inside the noble image rather than a user group, and Docker Desktop remaps bind-mount ownership in its own VM layer. The same question covers rootless Docker, where README already says to *drop* the flag.
|
|
||||||
- [ ] Map Kratos' 401 on a self-service flow init, so an anonymous `GET /settings` with no `?flow` renders instead of 500ing. `flowPage` (`src/auth/routes.ts`) maps 403/404/410 → restart the flow, 400 `session_already_available` → `/auth/complete`, and ≥500 → the themed 503, then rethrows everything else — and Kratos answers the settings-flow init with 401 when there is no session. `/settings` is correctly `public` (the recovery flow lands there with a live Kratos session but no app JWT), so the gate is not the fix: a 401 should redirect to `/login` with the page as `return_to`. No E2E covers an anonymous hit on a flow page that needs a session.
|
|
||||||
|
|
||||||
### Architectural review findings (2026-07-02)
|
`Bar = 1`
|
||||||
|
|
||||||
Prioritized. Overall verdict: architecture is sound; these are refinements.
|
`Next ID = 27`
|
||||||
|
|
||||||
- [ ] **MEDIUM — Add complexity/method-size static analysis to the CI gate.** Only `tsc --strict` today; a size/complexity rule would have caught the `app.ts` growth.
|
## Items
|
||||||
- [ ] **LOW — The users list offers a pencil "Edit" row action to a `users:read` holder.** The link is harmless (it opens the read-only detail page), but the label contradicts what the reader can do. Needs `canWrite` threaded into `listTable` plus a `common.view` core catalog key and an `i-eye` entry in `ICON_NAMES`.
|
|
||||||
- [ ] **MEDIUM→LOW — Add a list-page view-model helper in `src/ui/`.** Every list screen hand-rewrites the same ~40 lines bridging `parseListQuery`/`paginate` to the EJS partials; at minimum a `buildPaginationModel(page, hrefFor)` block.
|
|
||||||
- [ ] **LOW→MEDIUM — Retire `src/ui/shell-context.ts`.** `ShellModel`/`buildShellContext` has one consumer left (dashboard) and duplicates `PageChrome` on almost every field. Fold the dashboard onto `ctx.chrome` + title/breadcrumbs; keep `shellUser` as the shared primitive.
|
|
||||||
- [ ] **LOW — Fix stale doc references to removed `docs/plugin-contract.md`** in `views/index.ejs` (user-visible dashboard text; also links /scheduling as if pre-installed) and `examples/plugins/scheduling/views/shifts.ejs`.
|
|
||||||
- [ ] **LOW — Decide (once) on a `ctx.system` facade.** `@plainpages/plugin-api` exposes raw Ory client shapes, so an Ory client refactor is a major `apiVersion` bump. AGENTS.md accepts this; revisit only if external plugin authors appear.
|
|
||||||
- [ ] **LOW — README/AGENTS.md gaps:** state the intended lifetime/horizon explicitly, add a short domain glossary (host, manifest, chrome, nav fragment, permission token, system plugin, denylist…), and note the expected plugin-author population and plugin count — per-plugin dependency isolation prices N dependency trees on disk and in RSS, and that number is what says whether the trade needs revisiting (build-time dedupe for baked images stays open).
|
|
||||||
|
|
||||||
## Finnished work
|
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 9 | 0.4.21 | | question | **Decide whether the gate typechecks `e2e-tests/`.** | 1 | 1 | 1 | 1 | 3 | code health by 2 | 9.0 |
|
||||||
|
| 10 | 0.4.21 | | question | **Decide whether Playwright's `workers` is pinned.** | 1 | 1 | 1 | 1 | 1 | code health by 1 | 7.2 |
|
||||||
|
| 11 | 0.4.21 | | question | **Record the browser floor Plainpages supports, and whether the fallback below it is a contract or a courtesy.** | 1 | 1 | 1 | 1 | 2 | 2 by 2, 1 by 1 | 4.3 |
|
||||||
|
| 15 | 0.4.21 | | question | **Decide who owns `Cache-Control` for rendered pages: the host or the reverse proxy.** | 1 | 1 | 1 | 1 | 2 | 8 by 3 | 4.0 |
|
||||||
|
| 19 | 0.4.21 | | question | **Run the documented Docker commands on macOS and fix whatever misbehaves.** | 2 | 2 | 1 | 3 | 3 | 1 by 3 | 3.3 |
|
||||||
|
| 12 | 0.4.21 | | question | **Decide whether the one-item profile dropdown stays a dropdown.** | 1 | 1 | 1 | 1 | 2 | 2 by 2 | 2.8 |
|
||||||
|
| 18 | 0.4.21 | | question | **Decide whether the CSRF token stays unbound to the session.** | 1 | 1 | 1 | 1 | 2 | | 0.0 |
|
||||||
|
| 26 | 0.4.21 | | | **Add a domain glossary and the project's intended horizon to the README.** | 1 | 2 | 1 | 2 | 3 | 9 by 3, code health by 2 | 11.9 |
|
||||||
|
| 8 | 0.4.21 | | | **Run the visual E2E specs at viewports from a large desktop down to a small phone.** | 2 | 1 | 1 | 2 | 4 | code health by 3 | 9.0 |
|
||||||
|
| 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 |
|
||||||
|
| 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 |
|
||||||
|
| 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 |
|
||||||
|
| 25 | 0.4.21 | | | **Point the views' references to `docs/plugin-contract.md` at the README.** | 1 | 2 | 1 | 2 | 1 | 9 by 3 | 4.6 |
|
||||||
|
| 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 |
|
||||||
|
| 4 | 0.4.21 | | | **Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 |
|
||||||
|
| 7 | 0.4.21 | | | **Confirm a successful permission save, and report a partial failure instead of hiding it.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 |
|
||||||
|
| 22 | 0.4.21 | | | **Offer a `users:read` holder a View row action on the users list, not Edit.** | 2 | 2 | 1 | 3 | 3 | 2 by 2 | 2.1 |
|
||||||
|
| 3 | 0.4.21 | | | **Guard the group paths against self-lockout, or accept the gap explicitly.** | 3 | 2 | 2 | 5 | 6 | 2 by 6 | 1.4 |
|
||||||
|
| 1 | 0.4.21 | | owner | **Publish every release to Docker Hub, and give the Hub page its short description.** | 3 | 3 | 2 | 6 | 5 | 1 by 3 | 1.3 |
|
||||||
|
| 13 | 0.4.21 | | | **Accept a verification code pasted with surrounding whitespace.** | 4 | 2 | 3 | 7 | 7 | 2 by 8 | 1.0 |
|
||||||
|
| 16 | 0.5.0 | | question | **Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.** | 3 | 2 | 3 | 6 | 6 | 7 by 2 | 0.5 |
|
||||||
|
| 6 | 0.5.0 | | | **Warn at discovery when a route or nav node gates on a permission no plugin declares.** | 2 | 1 | 1 | 2 | 2 | 7 by 3, 9 by 3 | 8.0 |
|
||||||
|
| 23 | 0.5.0 | | | **Give list pages one view-model helper from query to pagination, instead of ~40 hand-written lines each.** | 3 | 1 | 2 | 4 | 5 | 1 by 4, 9 by 2, code health by 3 | 7.9 |
|
||||||
|
| 2 | 0.5.0 | | | **Rename the admin example plugin to a name that says what it administers.** | 3 | 1 | 2 | 4 | 6 | 9 by 3, code health by 2 | 2.6 |
|
||||||
|
| 5 | 0.5.0 | | | **List grants that no installed plugin declares, with a remove action.** | 2 | 2 | 2 | 4 | 4 | 2 by 3 | 2.1 |
|
||||||
|
| 14 | 0.5.0 | | | **Drop a double-clicked duplicate submit, with no client-side JavaScript.** | 4 | 2 | 3 | 7 | 7 | 2 by 7, 1 by 4 | 1.4 |
|
||||||
|
|
||||||
- [x] Configure a plugin at install time: the manifest declares `settings`, the operator sets one `PLUGIN_SETTING_<ID>_<KEY>` variable per key, and the resolved values arrive on `onBoot` typed from the declaration. Read-only admin screen at `/admin/plugin-settings`.
|
## Details
|
||||||
- [x] Give a plugin persistent storage: `storage: true` provisions a Postgres database + login role named `plugin_<id>`, credentials arrive on `onBoot`, passwords are derived from `PLUGIN_DB_SECRET` rather than stored.
|
|
||||||
- [x] Refuse a stray `package.json`/`node_modules` in `config/` by name, as plugin folders already are.
|
### 11. Record the browser floor Plainpages supports, and whether the fallback below it is a contract or a courtesy.
|
||||||
- [x] Let Renovate reach the example plugins' manifests (`ignorePaths` overrides `config:recommended`).
|
|
||||||
- [x] The seeded admin is granted each permission once — `seedPermissions` dedupes and the grant PUT is idempotent.
|
The stylesheet needs `:has()` (Dec 2023); the menus need the popover API (Safari 17) and CSS anchor
|
||||||
- [x] Run the E2E runner as the invoking user so its artifacts aren't root-owned.
|
positioning for placement, which is newer and unguarded, since the `@supports` test covers popover
|
||||||
- [x] Install node_modules above `WORKDIR /app` so no mount leaves a root-owned dir in the checkout.
|
only. An iPadOS 16 tablet gets panels flowing inline rather than working menus. Nobody has rendered
|
||||||
- [x] Enforce `<resource>:<action>` permission names at discovery; split `admin` per screen.
|
that path on real hardware.
|
||||||
- [x] Make the declared-permission catalog the fixed list; delete the Permissions screen, move granting onto Users and Groups.
|
|
||||||
- [x] Fail a Playwright test on any browser console warning, error or uncaught exception, in every engine.
|
### 15. Decide who owns `Cache-Control` for rendered pages: the host or the reverse proxy.
|
||||||
- [x] Skip the CI gate when only markdown changed.
|
|
||||||
- [x] Replace the `<details>` popup menus with the Popover API so an outside click dismisses them.
|
Responses carry `Vary: Accept-Language` but no `Cache-Control`, so a signed-in page is not marked
|
||||||
- [x] Organize the files in src into folders.
|
`private`. Either set the headers (public cacheable, gated `private, no-store`) or record in AGENTS.md
|
||||||
- [x] Move docs/plugin-contract.md into README.md and remove the docs folder.
|
that the reverse proxy owns this.
|
||||||
- [x] Move the scheduling example out of `plugins/` into `examples/`.
|
|
||||||
- [x] Make `config/` an empty drop-in mount with the defaults as fallback.
|
### 19. Run the documented Docker commands on macOS and fix whatever misbehaves.
|
||||||
- [x] Turn the built-in admin pages into a drop-in example plugin.
|
|
||||||
- [x] CI/CD — test on push to any branch except main.
|
Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id -g` is `20`, which is
|
||||||
- [x] CI/CD — require a PR to main, gated on a green build, fast-forward-only.
|
`dialout` inside the noble image, and Docker Desktop remaps bind-mount ownership in its VM.
|
||||||
- [x] CI/CD — force-push mirror to GitHub after every merge to main.
|
|
||||||
- [x] CI/CD — build and push the app image, tagged with the commit hash, as part of the gate.
|
### 12. Decide whether the one-item profile dropdown stays a dropdown.
|
||||||
- [x] CI/CD — re-tag the hash image to semver on a `vX.Y.Z` tag.
|
|
||||||
- [x] CI/CD — sync released tags to Docker Hub.
|
It holds Sign out behind a click, and its "Signed in as X" head repeats what the trigger shows.
|
||||||
- [x] Write README-dockerhub.md for the Docker Hub overview.
|
|
||||||
- [x] CI/CD — set up the Renovate bot.
|
### 18. Decide whether the CSRF token stays unbound to the session.
|
||||||
- [x] CI/CD — give Renovate a read-only `GITHUB_COM_TOKEN` so github.com lookups aren't rate-limited.
|
|
||||||
- [x] CI/CD — auto-release on Renovate updates, versioned from the `Release-Bump:` trailer.
|
`src/auth/csrf.ts` signs `<nonce>.<HMAC(secret, nonce)>` with no session binding, so any validly
|
||||||
- [x] Add an e2e test for the admin plugin's OAuth2-clients (Hydra) screen.
|
signed token passes for any user: an attacker who can write cookies on the origin can fix a token
|
||||||
- [x] Document the auth security model in the README.
|
they know. Standard for unbound signed double-submit, and plausibly fine behind `SameSite=Lax` and
|
||||||
- [x] Add i18n support.
|
HSTS. Accepted → record it in AGENTS.md and README → Security model; not accepted → bind the nonce to
|
||||||
- [x] Settle the identity-vs-user vocabulary.
|
`sub`.
|
||||||
- [x] Use one uniform verb per action in the English UI (sign in / sign out / create account).
|
|
||||||
- [x] Remove the dead "Profile" link from the sidebar profile menu.
|
### 26. Add a domain glossary and the project's intended horizon to the README.
|
||||||
- [x] Remove the unspecified "Settings"/"Preferences" cog from the sidebar footer.
|
|
||||||
- [x] **HIGH — Split `handleRequest` in `src/http/app.ts`** — extract the built-in endpoints into named handlers on an internal route table.
|
Glossary: host, manifest, chrome, nav fragment, permission token, system plugin, denylist. Also the
|
||||||
|
expected plugin count, since per-plugin dependency isolation prices N dependency trees on disk and
|
||||||
|
in RSS.
|
||||||
|
|
||||||
|
### 24. Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.
|
||||||
|
|
||||||
|
`ShellModel`/`buildShellContext` has one consumer, the dashboard, and duplicates `PageChrome` on
|
||||||
|
almost every field. Keep `shellUser` as the shared primitive.
|
||||||
|
|
||||||
|
### 17. Remove the icons no caller uses from `ICON_NAMES` and the sprite.
|
||||||
|
|
||||||
|
`i-chart`, `i-copy` and `i-download` have no caller, and the sprite is inlined into every page.
|
||||||
|
AGENTS.md → "`ICON_NAMES` is a host-owned registry" already makes this routine cleanup.
|
||||||
|
|
||||||
|
### 21. Fail the gate on a function that exceeds a size or complexity limit.
|
||||||
|
|
||||||
|
Only `tsc --strict` runs today. A new tool is a new dependency, so justify it.
|
||||||
|
|
||||||
|
### 25. Point the views' references to `docs/plugin-contract.md` at the README.
|
||||||
|
|
||||||
|
`examples/plugins/scheduling/views/shifts.ejs` still cites it.
|
||||||
|
|
||||||
|
### 20. Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.
|
||||||
|
|
||||||
|
`flowPage` (`src/auth/routes.ts`) rethrows a 401, which Kratos answers to a settings-flow init with no
|
||||||
|
session. `/settings` stays `public`, since recovery lands there with a Kratos session but no app JWT;
|
||||||
|
a 401 should redirect to `/login` with the page as `return_to`. Add the missing E2E.
|
||||||
|
|
||||||
|
### 4. Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.
|
||||||
|
|
||||||
|
Sketch: post the rendered set as a hidden baseline; when it no longer matches Keto, re-render with
|
||||||
|
"this changed while you had the page open" rather than applying.
|
||||||
|
|
||||||
|
### 7. Confirm a successful permission save, and report a partial failure instead of hiding it.
|
||||||
|
|
||||||
|
`applyGrants` loops writes then deletes with no transaction, so a Keto error midway leaves a
|
||||||
|
half-applied set behind the generic error page. The `alert alert-pos` pattern of the recovery-code
|
||||||
|
banner is available.
|
||||||
|
|
||||||
|
### 22. Offer a `users:read` holder a View row action on the users list, not Edit.
|
||||||
|
|
||||||
|
The pencil opens the read-only detail page, so it is harmless, but the label contradicts what the
|
||||||
|
reader can do. Needs `canWrite` threaded into `listTable`, a `common.view` core catalog key and an
|
||||||
|
`i-eye` icon.
|
||||||
|
|
||||||
|
### 3. Guard the group paths against self-lockout, or accept the gap explicitly.
|
||||||
|
|
||||||
|
The self-revoke guard covers only your own direct grants on the Users screen. Unticking a permission
|
||||||
|
on a group you belong to, leaving that group, or deleting it can still strip your own access with no
|
||||||
|
warning. AGENTS.md records this as a known gap. The robust fix is a "last effective holder" check,
|
||||||
|
which needs a reverse Keto query.
|
||||||
|
|
||||||
|
### 1. Publish every release to Docker Hub, and give the Hub page its short description.
|
||||||
|
|
||||||
|
Hub's newest tag is 0.4.0, while git has v0.4.1 onwards, and the overview still says 0.4.0. No
|
||||||
|
`release.yml` run exists for the tags auto-release pushes as `renovate-bot`; find out why Gitea
|
||||||
|
doesn't fire `push: tags` for them, fix it, and backfill the missing tags. fejkdata did this
|
||||||
|
(`../fejkdata`, commits `51d924a` and `34c8234`), and two parts port directly: a one-line
|
||||||
|
description riding the existing PATCH as its `description` field, and a unit test that the
|
||||||
|
description opens a README sentence and every command the overview shows appears in the README.
|
||||||
|
fejkdata also syncs the page on any push to `main`, with its own token in an environment only `main`
|
||||||
|
may use; decide whether that fits here, where `{{VERSION}}` must render a released tag.
|
||||||
|
|
||||||
|
### 13. Accept a verification code pasted with surrounding whitespace.
|
||||||
|
|
||||||
|
A pasted code fails the browser's `pattern` check. No zero-JS fix exists: `pattern`
|
||||||
|
cannot transform, so loosening it only lets the untrimmed value through to Kratos. **Check first:**
|
||||||
|
`kratos-admin.ts` returns a `recovery_link` alongside the code; if the stock courier mail can carry
|
||||||
|
one, a template change fixes this with no host code. Otherwise proxy the flow POST through the host:
|
||||||
|
`submitFlow` (`src/auth/kratos-public.ts`) already relays cookies and normalises a 422
|
||||||
|
`redirect_browser_to`, and has no callers outside tests. CSRF improves, since the POST becomes
|
||||||
|
same-origin and `form-action 'self'` can join the CSP. Risks: one `flow.ui.action` serves all five
|
||||||
|
flows, so this rewrites the sign-in path; and Go's nosurf checks Referer only over https, which no
|
||||||
|
test covers. Code entry has no E2E coverage today.
|
||||||
|
|
||||||
|
### 16. Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.
|
||||||
|
|
||||||
|
Ory's own examples model `Shift`, `Document` and so on. One global `Resource` bucket is the
|
||||||
|
project's "no catch-all names" rule applied to namespaces.
|
||||||
|
|
||||||
|
### 6. Warn at discovery when a route or nav node gates on a permission no plugin declares.
|
||||||
|
|
||||||
|
Declaring stays optional on purpose, but the picker is built from declarations only, so such a route
|
||||||
|
is ungrantable from the GUI and answers a permanent 403. A warning, not an error.
|
||||||
|
|
||||||
|
### 23. Give list pages one view-model helper from query to pagination, instead of ~40 hand-written lines each.
|
||||||
|
|
||||||
|
Every list screen bridges `parseListQuery`/`paginate` to the partials by hand. At minimum a
|
||||||
|
`buildPaginationModel(page, hrefFor)`. Exported through the barrel, so it is contract surface.
|
||||||
|
|
||||||
|
### 2. Rename the admin example plugin to a name that says what it administers.
|
||||||
|
|
||||||
|
Candidates: `auth-admin`, `users-groups-admin`. The id is also the mount path, so the compose files,
|
||||||
|
E2E stacks and README move with it.
|
||||||
|
|
||||||
|
### 5. List grants that no installed plugin declares, with a remove action.
|
||||||
|
|
||||||
|
`grantDiff` never revokes an undeclared name, and nothing shows one either, so it can't be audited or
|
||||||
|
cleaned, and reinstalling its plugin silently reactivates access nobody remembers granting.
|
||||||
|
|
||||||
|
### 14. Drop a double-clicked duplicate submit, with no client-side JavaScript.
|
||||||
|
|
||||||
|
A second identical POST is expected from a non-technical user, and today creates two
|
||||||
|
users, mints two recovery codes, or registers two OAuth2 clients. It must not break a legitimately
|
||||||
|
repeatable action. Sketch: a CSS-only affordance so the second click has nothing to hit, plus the
|
||||||
|
host recognising a duplicate (same session, route and payload within a short window), logging and
|
||||||
|
dropping it. Open: what identifies "the same submission" (a one-time token minted into each form
|
||||||
|
beats hashing the payload), the window, where the record lives given the host is stateless, and how
|
||||||
|
a plugin declares a route repeatable.
|
||||||
|
|||||||
Reference in New Issue
Block a user