From edf0f7c6d1a20de097addce7a0854564d9c7605a Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 30 Oct 2026 19:11:00 +0100 Subject: [PATCH] Plan: cache stylesheets and scripts under /public/ until their content changes --- todo.md | 1 + 1 file changed, 1 insertion(+) diff --git a/todo.md b/todo.md index ee14ba8..fef843b 100644 --- a/todo.md +++ b/todo.md @@ -17,6 +17,7 @@ - [ ] 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. +- [ ] **Cache every stylesheet and script in the browser until its content changes, so a page view downloads none of them again.** Nothing under `/public/` carries a `Cache-Control` header (`serveStatic`, `src/http/static.ts`), so every page view downloads `styles.css`, `auth.css` and each plugin's stylesheet again: about 50 kB on a plugin's issue page. On a GPRS connection that took seconds per page (measured in Firefox, 2026-10-30), and in those seconds a browser extension that forces layout early makes Firefox paint the page unstyled ("Layout was forced before the page was fully loaded"). Sketch: put a content version in each asset's URL, as a query (`?v=`) or in the path, and answer a versioned URL with `Cache-Control: public, max-age=31536000, immutable`, so a release still reaches every browser at once. The shell's ``s, the error pages' and the `styles` and script hrefs a plugin passes in (`views/partials/shell.ejs`) all need to come out versioned, so a plugin names its asset once and the host adds the version. Open: whether the host versions a plugin's hrefs itself or hands plugins a helper, and what an unversioned `/public/` URL answers (a short `max-age` with revalidation is the safe default). Requested by plainpages-plugin-fastjira, whose item 142 waits on the release that ships 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 `.` 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`.