Compare commits

..

2 Commits

Author SHA1 Message Date
lilleman edf0f7c6d1 Plan: cache stylesheets and scripts under /public/ until their content changes
CI / full-gate (push) Successful in 4s
2026-10-30 19:11:00 +01:00
renovate-bot b81c136fe5 Update dependency lucide-static to v1.64.0
CI / full-gate (push) Successful in 5m12s
Mirror / github-mirror (push) Successful in 4s
Release-Bump: minor
2026-10-30 04:18:42 +00:00
3 changed files with 11 additions and 10 deletions
+8 -8
View File
@@ -7,8 +7,8 @@
"name": "plainpages", "name": "plainpages",
"dependencies": { "dependencies": {
"@larvit/log": "2.3.0", "@larvit/log": "2.3.0",
"ejs": "7.0.2", "ejs": "6.0.1",
"lucide-static": "1.62.0", "lucide-static": "1.64.0",
"postgres": "3.4.9" "postgres": "3.4.9"
}, },
"devDependencies": { "devDependencies": {
@@ -387,9 +387,9 @@
} }
}, },
"node_modules/ejs": { "node_modules/ejs": {
"version": "7.0.2", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-7.0.2.tgz", "resolved": "https://registry.npmjs.org/ejs/-/ejs-6.0.1.tgz",
"integrity": "sha512-LcDSEP6Oclb9sTiSyG0MpG0RuKvLDRxCOOt+2sIk+kdk1rMkFU8SRnT0qaNt5zvU0LOV1YXXOvIoZL/sKwabvw==", "integrity": "sha512-UaaM14yby8U3k02ihS1Bmj5Kz2d7CCQM1scxpgs4Mhkq8F1wR2gl3+Ts4h5Ne4Mnt7M9m4Dw7jsuMr3+xO4vZA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"ejs": "bin/cli.js" "ejs": "bin/cli.js"
@@ -399,9 +399,9 @@
} }
}, },
"node_modules/lucide-static": { "node_modules/lucide-static": {
"version": "1.62.0", "version": "1.64.0",
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.62.0.tgz", "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.64.0.tgz",
"integrity": "sha512-WBZA/m2PvkFpMGHDtsBck5P2A9G4ehLlYu4GwwICcmEm1BvYVgj00lCDxKRWLlbMlOibLaiwdQ88G5pHkSWgwA==", "integrity": "sha512-UxWmMteOXbKHI5E4f2qVtxCAAsI9gsInYK1lNw0nrOA0w4FkJeOVZ+UOV6aGA2377FZe3qyiFYq3lqjBKZ6OuA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/postgres": { "node_modules/postgres": {
+2 -2
View File
@@ -17,8 +17,8 @@
}, },
"dependencies": { "dependencies": {
"@larvit/log": "2.3.0", "@larvit/log": "2.3.0",
"ejs": "7.0.2", "ejs": "6.0.1",
"lucide-static": "1.62.0", "lucide-static": "1.64.0",
"postgres": "3.4.9" "postgres": "3.4.9"
}, },
"devDependencies": { "devDependencies": {
+1
View File
@@ -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. - [ ] 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. - [ ] 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 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=<hash of the file>`) 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 `<link>`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 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 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`. - [ ] 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`.