Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edf0f7c6d1 | |||
| b81c136fe5 | |||
| c9e25f2852 | |||
| 603e29e8f1 | |||
| 055bfd6506 | |||
| 80cc692634 | |||
| ebf34deda4 | |||
| 937a0739ca | |||
| 44813be8a7 | |||
| 785230673b | |||
| 80981eb791 | |||
| 0097481b4c | |||
| b09f8b7eb6 | |||
| fb464acf54 | |||
| 8220b34fd0 | |||
| 194557fb49 | |||
| 4a0fc2b493 | |||
| 946c45663f | |||
| 41ab968b20 | |||
| ff548a30d2 | |||
| f80c6c6426 | |||
| 3658797764 | |||
| 23f1158211 | |||
| bf9302ce8e | |||
| 2ed134fc86 | |||
| e07edeb6ae |
@@ -19,4 +19,4 @@ jobs:
|
||||
run: |
|
||||
docker run --rm -v "$PWD:/repo" -w /repo \
|
||||
-e REGISTRY_TOKEN -e REGISTRY_USER -e REPO_TOKEN -e REPOSITORY -e SERVER_URL \
|
||||
node:24.21.0-alpine3.24 node registry-cleanup/cleanup.ts
|
||||
node:24.22.2-alpine3.24 node registry-cleanup/cleanup.ts
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
GIT_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm -v "$PWD:/repo" -w /repo node:24.21.0-alpine3.24 \
|
||||
docker run --rm -v "$PWD:/repo" -w /repo node:24.22.2-alpine3.24 \
|
||||
node release-tooling/contract-version.ts "$GIT_TAG" src/plugin-host/plugin.ts
|
||||
- name: Promote the commit-hash image to semver + latest
|
||||
env:
|
||||
@@ -98,9 +98,9 @@ jobs:
|
||||
VERSION=${INPUT_VERSION:-${GIT_TAG#v}}
|
||||
VERSION=${VERSION#v}
|
||||
# An empty dispatch input falls back to the branch name, so gate this like a tag.
|
||||
docker run --rm -v "$PWD:/repo" -w /repo node:24.21.0-alpine3.24 \
|
||||
docker run --rm -v "$PWD:/repo" -w /repo node:24.22.2-alpine3.24 \
|
||||
node release-tooling/contract-version.ts "$VERSION" src/plugin-host/plugin.ts
|
||||
docker run --rm -v "$PWD:/repo" -w /repo \
|
||||
-e DOCKERHUB_REPO -e DOCKERHUB_TOKEN -e DOCKERHUB_USER \
|
||||
node:24.21.0-alpine3.24 \
|
||||
node:24.22.2-alpine3.24 \
|
||||
node release-tooling/dockerhub-overview.ts "$VERSION"
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
-e RENOVATE_PLATFORM=gitea \
|
||||
-e RENOVATE_REPOSITORIES=${{ github.repository }} \
|
||||
-e RENOVATE_TOKEN \
|
||||
renovate/renovate:44.117.2
|
||||
renovate/renovate:44.186.6
|
||||
|
||||
# After the renovate job, cut ONE tag covering the renovate-bot commits merged to main since the
|
||||
# last tag (batch per run). Targets origin/main — the real post-merge tip; the checkout SHA is the
|
||||
@@ -58,11 +58,11 @@ jobs:
|
||||
if [ -z "$BUMPS" ]; then
|
||||
echo "Renovate commits since ${LATEST}, but none carry Release-Bump — nothing reached a running Plainpages; skipping"; exit 0
|
||||
fi
|
||||
NEXT=$(docker run --rm -v "$PWD:/repo" -w /repo node:24.21.0-alpine3.24 \
|
||||
NEXT=$(docker run --rm -v "$PWD:/repo" -w /repo node:24.22.2-alpine3.24 \
|
||||
node release-tooling/next-version.ts "$LATEST" $BUMPS)
|
||||
# Read the constant off origin/main, not the checkout, which lags the merges this run made.
|
||||
git show origin/main:src/plugin-host/plugin.ts \
|
||||
| docker run -i --rm -v "$PWD:/repo" -w /repo node:24.21.0-alpine3.24 \
|
||||
| docker run -i --rm -v "$PWD:/repo" -w /repo node:24.22.2-alpine3.24 \
|
||||
node release-tooling/contract-version.ts "$NEXT" -
|
||||
echo "Releasing $LATEST -> $NEXT"
|
||||
git tag "$NEXT" origin/main
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Node 24 runs TypeScript directly (type stripping) — no build step. Pinned exact tag.
|
||||
FROM node:24.21.0-alpine3.24
|
||||
FROM node:24.22.2-alpine3.24
|
||||
|
||||
# Above WORKDIR so dev's `.:/app` bind mount can't shadow them; a volume at /app/node_modules
|
||||
# instead leaves a root-owned dir in the checkout (the daemon creates mount destinations as root).
|
||||
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
# backs it (PLUGIN_SETTING_SCHEDULING_UPSTREAM above points here). Stand-in for the customer's real service —
|
||||
# stdlib-only, in-memory, no auth. Prod points PLUGIN_SETTING_SCHEDULING_UPSTREAM at the real backend instead.
|
||||
shifts-upstream:
|
||||
image: node:24.21.0-alpine3.24
|
||||
image: node:24.22.2-alpine3.24
|
||||
command: node /srv/server.ts
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
# Dev mail catcher — Kratos recovery/verification emails land here (web UI on 8025).
|
||||
# kratos.yml points the courier at smtp://mailpit:1025; prod uses a real SMTP via env.
|
||||
mailpit:
|
||||
image: axllent/mailpit:v1.31.4
|
||||
image: axllent/mailpit:v1.32.0
|
||||
ports:
|
||||
- "8025:8025"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Playwright runner — browsers preinstalled, pinned to match @playwright/test in e2e-tests/.
|
||||
# Built/run via e2e-tests/compose.visual.yml; targets the `web` service over the network.
|
||||
FROM mcr.microsoft.com/playwright:v1.63.0-noble
|
||||
FROM mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
|
||||
WORKDIR /e2e-tests
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
|
||||
# The reference plugin's upstream (examples/shifts-upstream) so /scheduling/shifts shows real rows.
|
||||
shifts-upstream:
|
||||
image: node:24.21.0-alpine3.24
|
||||
image: node:24.22.2-alpine3.24
|
||||
command: ["node", "/server.ts"]
|
||||
volumes:
|
||||
- ./examples/shifts-upstream/server.ts:/server.ts:ro
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
# Mock OIDC provider for the SSO login test — stdlib Node, auto-approves, signs an id_token Kratos
|
||||
# verifies via its jwks. Reachable as the same host (mock-oidc:9000) by both the browser and Kratos.
|
||||
mock-oidc:
|
||||
image: node:24.21.0-alpine3.24
|
||||
image: node:24.22.2-alpine3.24
|
||||
command: ["node", "/mock-oidc.ts"]
|
||||
environment:
|
||||
ISSUER: http://mock-oidc:9000
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
|
||||
# Same-origin gateway: Kratos-owned paths → kratos, everything else → web (e2e-tests/proxy.ts).
|
||||
proxy:
|
||||
image: node:24.21.0-alpine3.24
|
||||
image: node:24.22.2-alpine3.24
|
||||
command: ["node", "/proxy.ts"]
|
||||
depends_on:
|
||||
web:
|
||||
|
||||
Generated
+12
-12
@@ -6,17 +6,17 @@
|
||||
"": {
|
||||
"name": "plainpages-e2e",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.63.0"
|
||||
"@playwright/test": "1.64.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
|
||||
"integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
|
||||
"integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.63.0"
|
||||
"playwright": "1.64.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -26,13 +26,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
|
||||
"integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
|
||||
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.63.0"
|
||||
"playwright-core": "1.64.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -42,9 +42,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
|
||||
"integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
|
||||
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"test": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.63.0"
|
||||
"@playwright/test": "1.64.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+8
-8
@@ -8,12 +8,12 @@
|
||||
"dependencies": {
|
||||
"@larvit/log": "2.3.0",
|
||||
"ejs": "6.0.1",
|
||||
"lucide-static": "1.52.0",
|
||||
"lucide-static": "1.64.0",
|
||||
"postgres": "3.4.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "3.1.5",
|
||||
"@types/node": "24.19.1",
|
||||
"@types/node": "24.19.2",
|
||||
"typescript": "7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -37,9 +37,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz",
|
||||
"integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==",
|
||||
"version": "24.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz",
|
||||
"integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -399,9 +399,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-static": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz",
|
||||
"integrity": "sha512-0zZYihAze4ZmUCcyNa9OkWYqn/Y5f9AdTh02AZIxMtP5ravcTTfoSIAnUPBH6ZvKxt+ZW28JB0ldzLRyD82fcw==",
|
||||
"version": "1.64.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.64.0.tgz",
|
||||
"integrity": "sha512-UxWmMteOXbKHI5E4f2qVtxCAAsI9gsInYK1lNw0nrOA0w4FkJeOVZ+UOV6aGA2377FZe3qyiFYq3lqjBKZ6OuA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postgres": {
|
||||
|
||||
+2
-2
@@ -18,12 +18,12 @@
|
||||
"dependencies": {
|
||||
"@larvit/log": "2.3.0",
|
||||
"ejs": "6.0.1",
|
||||
"lucide-static": "1.52.0",
|
||||
"lucide-static": "1.64.0",
|
||||
"postgres": "3.4.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "3.1.5",
|
||||
"@types/node": "24.19.1",
|
||||
"@types/node": "24.19.2",
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ services:
|
||||
|
||||
# Catches Kratos' recovery/verification emails — UI on http://localhost:8025
|
||||
mailpit:
|
||||
image: axllent/mailpit:v1.31.4
|
||||
image: axllent/mailpit:v1.32.0
|
||||
ports:
|
||||
- "8025:8025"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## Unfinnished work
|
||||
|
||||
- [ ] **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.
|
||||
- [ ] 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.
|
||||
@@ -16,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=<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 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`.
|
||||
|
||||
Reference in New Issue
Block a user