From bb09b3a82127e5f0d92d40139115a02b674d5046 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 00:25:29 +0100 Subject: [PATCH 1/7] Record the reading cost of the chunk --- docs/reading-cost.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/reading-cost.md b/docs/reading-cost.md index cd54ac5..78bf6e4 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -530,3 +530,20 @@ Read most (opens and searches): - `docs/reading-cost.md` 2 - `examples/plugins/auth-admin/admin-grants.ts` 2 - `examples/plugins/auth-admin/i18n/en-US.ts` 2 + +## Reading cost, chunk a2d8600ec28b, finished 2026-11-08T23:23:17Z + +66451 new input tokens before the first commit. + +Read most (opens and searches): + +- `src/auth/bootstrap.ts` 4 +- `src/auth/jwt-middleware.ts` 4 +- `README.md` 3 +- `ci.sh` 3 +- `examples/plugins/scheduling/plugin.ts` 3 +- `src/auth/jwt.ts` 3 +- `src/ui/chrome.ts` 3 +- `compose.yml` 2 +- `examples/plugins/auth-admin/admin-groups.ts` 2 +- `examples/plugins/auth-admin/views/partials/group-detail-body.ejs` 2 -- 2.52.0 From 6fe7ba3b6d78261861086c27cea51504851e395a Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 00:25:29 +0100 Subject: [PATCH 2/7] Return the landing slots' results to the request pipeline instead of sending them --- src/function-limits.test.ts | 2 +- src/http/app-plugins.test.ts | 25 ++++++++++++- src/http/app.ts | 68 ++++++++++++++++++++---------------- src/http/builtin-route.ts | 15 +++++--- 4 files changed, 74 insertions(+), 36 deletions(-) diff --git a/src/function-limits.test.ts b/src/function-limits.test.ts index 43c41e5..4b5f423 100644 --- a/src/function-limits.test.ts +++ b/src/function-limits.test.ts @@ -13,7 +13,7 @@ const LIMITS = { lines: 50, complexity: 20 }; // Each function over a limit, at its measurement. A ceiling follows its function down, and a metric // leaves the entry once it fits, so a function that shrank can't grow back. const grandfathered: Record = { - "src/http/app.ts createApp": { lines: 144, complexity: 26 }, + "src/http/app.ts createApp": { lines: 135, complexity: 26 }, "src/auth/flow-view.ts buildFlowView": { complexity: 24 }, "src/auth/hydra-admin.ts createHydraAdmin": { lines: 87 }, "src/auth/kratos-admin.ts createKratosAdmin": { lines: 68 }, diff --git a/src/http/app-plugins.test.ts b/src/http/app-plugins.test.ts index 1f5309c..3f3ee9b 100644 --- a/src/http/app-plugins.test.ts +++ b/src/http/app-plugins.test.ts @@ -15,7 +15,7 @@ import { KratosError, type Session } from "../auth/kratos-public.ts"; import { SESSION_COOKIE } from "../auth/login.ts"; import { createLogger } from "../logger.ts"; import type { Plugin } from "../plugin-host/plugin.ts"; -import { ecJwk, fakeKeto, mintJwt, startApp, stubAdmin, withWhoami } from "../test-tooling/app-harness.ts"; +import { ecJwk, fakeKeto, mintJwt, session, startApp, stubAdmin, withWhoami } from "../test-tooling/app-harness.ts"; // A test plugin exercising each RouteResult shape, a path param, and the permission gate. const demoPlugin: Plugin = { @@ -350,6 +350,29 @@ test("plugin hooks: onRequest can short-circuit a request and onResponse observe assert.ok(seen.includes("/hooked/ok:handler ran")); }); +test("plugin hooks: onResponse observes the result of a plugin-owned `/` and `/dashboard`", async (t) => { + const seen: string[] = []; + const owner: Plugin = { + apiVersion: "1.0.0", + dashboard: () => ({ html: "owned dashboard" }), + home: () => ({ html: "owned home" }), + id: "owner", + }; + const observer: Plugin = { + apiVersion: "1.0.0", + hooks: { onResponse: (c, r) => void seen.push(`${c.url.pathname}:${r && "html" in r ? r.html : "?"}`) }, + id: "observer", + }; + const app = createApp({ jwks: staticJwks([ecJwk]), plugins: [owner, observer] }); + await new Promise((r) => app.listen(0, r)); + t.after(() => app.close()); + const url = `http://localhost:${(app.address() as AddressInfo).port}`; + + assert.match(await (await fetch(url + "/")).text(), /owned home/); + assert.match(await (await fetch(url + "/dashboard", { headers: { cookie: session() } })).text(), /owned dashboard/); + assert.deepEqual(seen, ["/:owned home", "/dashboard:owned dashboard"]); +}); + test("plugin hooks: the request routes by the method and path it arrived with, whatever onRequest does to req", async (t) => { const rewriter: Plugin = { apiVersion: "1.0.0", diff --git a/src/http/app.ts b/src/http/app.ts index 7987776..685e851 100644 --- a/src/http/app.ts +++ b/src/http/app.ts @@ -3,7 +3,7 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import ejs from "ejs"; -import { type BuiltinRoute, matchBuiltinRoute, type PluginContextFactory, type RequestCsrf } from "./builtin-route.ts"; +import { type BuiltinRoute, matchBuiltinRoute, type PluginContextFactory, type PluginOwned, type RequestCsrf } from "./builtin-route.ts"; import { buildPluginChrome, type PageChrome } from "../ui/chrome.ts"; import { buildContext, type RequestContext, type User } from "./context.ts"; import { csrfCookie, ensureCsrfToken, verifyCsrfRequest } from "../auth/csrf.ts"; @@ -140,32 +140,22 @@ export function createApp(options: AppOptions = {}): Server { const pluginViewsFor = (ctx: RequestContext, id: string): ViewRenderer => (view, data) => renderView(id, view, { ...data, ...i18nLocals(localsOf(ctx)), assetHref: assets.href }); // The public landing "/", ungated. A plugin may own it via `home`; else the built-in intro page. - const serveHome = async (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory): Promise => { + const serveHome = async (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory): Promise => { csrf.setCookie(); - if (homePlugin) { - // The plugin owns this page, so it runs on its own context — its catalog first, then core. - const pluginCtx = contextFor(homePlugin.id); - const result = (await homePlugin.home(pluginCtx)) ?? null; - if (anyResponseHooks) await runResponseHooks(plugins, contextFor, result); - await sendResult(ctx.res, result, pluginViewsFor(pluginCtx, homePlugin.id), pluginCtx.localeHref); - return null; - } - return { data: { chrome: ctx.chrome, user: ctx.user }, view: "home" }; + if (!homePlugin) return { data: { chrome: ctx.chrome, user: ctx.user }, view: "home" }; + // The plugin owns this page, so it runs on its own context — its catalog first, then core. + const pluginCtx = contextFor(homePlugin.id); + return { ctx: pluginCtx, pluginId: homePlugin.id, result: (await homePlugin.home(pluginCtx)) ?? null }; }; // "/dashboard", gated to a signed-in user. A plugin may own it via `dashboard`; else the built-in // starter page. - const serveDashboard = async (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory): Promise => { + const serveDashboard = async (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory): Promise => { // The page carries the Sign-out form, so Set-Cookie a fresh CSRF token here when absent. csrf.setCookie(); - if (dashboardPlugin) { - const pluginCtx = contextFor(dashboardPlugin.id); // as serveHome: the owner's own translator - const result = (await dashboardPlugin.dashboard(pluginCtx)) ?? null; - if (anyResponseHooks) await runResponseHooks(plugins, contextFor, result); - await sendResult(ctx.res, result, pluginViewsFor(pluginCtx, dashboardPlugin.id), pluginCtx.localeHref); - return null; - } - return { data: { chrome: ctx.chrome }, view: "dashboard" }; + if (!dashboardPlugin) return { data: { chrome: ctx.chrome }, view: "dashboard" }; + const pluginCtx = contextFor(dashboardPlugin.id); // as serveHome: the owner's own translator + return { ctx: pluginCtx, pluginId: dashboardPlugin.id, result: (await dashboardPlugin.dashboard(pluginCtx)) ?? null }; }; // The internal route table, matched after plugin routes: the auth/OAuth2 group (src/auth/ @@ -178,8 +168,9 @@ export function createApp(options: AppOptions = {}): Server { const host: Host = { anyRequestHooks, anyResponseHooks, assets, authOptions, builtinRoutes, csrfSecret, i18n, jwks, keto, kratos, kratosAdmin, menu, permissionCatalog, pluginViewsFor, plugins, secureCookies, settingsCatalog, system, viewsFor }; - // A stage that returns a boolean returns true once it has answered the request, and false to - // pass the request to the next stage. + // Static files, the canonical-host redirect and prepareRequest come first; then the answering + // stages in order — onRequest hook, plugin route, built-in route, else 404/405. A stage that + // returns a boolean returns true once it has answered the request, and false to pass it on. const handleRequest = async (req: IncomingMessage, res: ServerResponse, reqLog: Log): Promise => { // Error pages can render before this request has a context at all (a throw on the way to one), // so they start on the built-in English and switch to the visitor's locale once it is resolved. @@ -279,17 +270,16 @@ async function prepareRequest(host: Host, req: IncomingMessage, res: ServerRespo const carryLocale = (href: string): string => localeHref(href, explicit ? locale : null); const t = i18n.translator(locale); const user = await resolveUser(host, req, res, reqLog); - const { fresh, token } = ensureCsrfToken(req.headers.cookie, host.csrfSecret); - const csrf: RequestCsrf = { - setCookie: (): void => { if (fresh) res.appendHeader("set-cookie", csrfCookie(token, { secure: host.secureCookies })); }, - token, - }; + const csrf = requestCsrf(host, req, res); const verifyCsrf = (submitted: string | null | undefined): boolean => verifyCsrfRequest({ cookieHeader: req.headers.cookie, secret: host.csrfSecret, submitted }); // Chrome composes the whole menu, so it is memoized and resolved lazily — a json/redirect // handler, or the public "/" with a standalone home, never pays for it. let chromeMemo: PageChrome | undefined; - const chrome = (): PageChrome => (chromeMemo ??= buildPluginChrome({ csrfToken: token, currentPath: url.pathname, localeHref: carryLocale, menu: host.menu, plugins: host.plugins, t, translatorFor: (id) => i18n.translator(locale, id), user })); + const chrome = (): PageChrome => (chromeMemo ??= buildPluginChrome({ + csrfToken: csrf.token, currentPath: url.pathname, localeHref: carryLocale, menu: host.menu, plugins: host.plugins, t, + translatorFor: (id) => i18n.translator(locale, id), user, + })); const shared = { assetHref: host.assets.href, chrome, declaredPermissions: host.permissionCatalog, declaredSettings: host.settingsCatalog, locale, localeHref: carryLocale, locales: i18n.available, log: reqLog, user, verifyCsrf, ...(host.system ? { system: host.system } : {}) }; return { carryLocale, @@ -302,6 +292,14 @@ async function prepareRequest(host: Host, req: IncomingMessage, res: ServerRespo }; } +function requestCsrf(host: Host, req: IncomingMessage, res: ServerResponse): RequestCsrf { + const { fresh, token } = ensureCsrfToken(req.headers.cookie, host.csrfSecret); + return { + setCookie: (): void => { if (fresh) res.appendHeader("set-cookie", csrfCookie(token, { secure: host.secureCookies })); }, + token, + }; +} + // A lapsed token still backed by a live Kratos session is silently re-minted — "stay signed // in". The only place the hot path touches Ory. async function resolveUser(host: Host, req: IncomingMessage, res: ServerResponse, reqLog: Log): Promise { @@ -355,8 +353,18 @@ async function answerBuiltinRoute(host: Host, state: RequestState): Promise RequestContext; +// A landing slot's result when a plugin owns it: rendered against that plugin's views, and +// observed by the response hooks, on `ctx`. +export interface PluginOwned { + ctx: RequestContext; // the owner's own context, from PluginContextFactory + pluginId: string; + result: RouteResult | null; +} + export interface BuiltinRoute extends Gate { - // Returns a RouteResult, or null when the handler wrote to ctx.res itself - // (the landing slots dispatch a plugin's own result against that plugin's views). - handler: (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory) => Promise | RouteResult | null; + // Returns a RouteResult, or null when the handler wrote to ctx.res itself. + handler: (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory) => Promise | RouteResult | PluginOwned | null; method: "GET" | "POST"; // a GET route also answers HEAD, like plugin routes path: string; // exact pathname } -- 2.52.0 From 30e07e0044790027e397eaa3954eb21fb921d796 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 00:26:26 +0100 Subject: [PATCH 3/7] =?UTF-8?q?Answer=20security=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=200=20=F0=9F=9F=A1=20(0=20misread);=20no=20findin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -- 2.52.0 From 0116e862baf7f9a341c6356b694b1d268c6d2958 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 00:26:32 +0100 Subject: [PATCH 4/7] Reword the PluginOwned comment --- src/http/builtin-route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http/builtin-route.ts b/src/http/builtin-route.ts index c1b89e5..1abfbd8 100644 --- a/src/http/builtin-route.ts +++ b/src/http/builtin-route.ts @@ -20,8 +20,8 @@ export interface RequestCsrf { // own context — otherwise the plugin's keys render as bare keys on the pages it owns. export type PluginContextFactory = (pluginId: string) => RequestContext; -// A landing slot's result when a plugin owns it: rendered against that plugin's views, and -// observed by the response hooks, on `ctx`. +// A landing slot's result when a plugin owns it: the host renders it against that plugin's views +// on `ctx`, and shows it to the response hooks like a plugin route's result. export interface PluginOwned { ctx: RequestContext; // the owner's own context, from PluginContextFactory pluginId: string; -- 2.52.0 From 4311944a249bf86e0d90d64f29d1ee8fd2d43dcc Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 00:51:27 +0100 Subject: [PATCH 5/7] =?UTF-8?q?Answer=20stability=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=204=20=F0=9F=9F=A1=20(0=20misread);=20drop=20the?= =?UTF-8?q?=20null=20built-in=20result,=20pin=20the=20owner's=20views=20an?= =?UTF-8?q?d=20locale=20on=20the=20landing=20slots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/function-limits.test.ts | 2 +- src/http/app-i18n.test.ts | 18 ++++++++++++++++-- src/http/app.ts | 11 ++++------- src/http/builtin-route.test.ts | 2 +- src/http/builtin-route.ts | 11 ++++------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/function-limits.test.ts b/src/function-limits.test.ts index 4b5f423..87287be 100644 --- a/src/function-limits.test.ts +++ b/src/function-limits.test.ts @@ -13,7 +13,7 @@ const LIMITS = { lines: 50, complexity: 20 }; // Each function over a limit, at its measurement. A ceiling follows its function down, and a metric // leaves the entry once it fits, so a function that shrank can't grow back. const grandfathered: Record = { - "src/http/app.ts createApp": { lines: 135, complexity: 26 }, + "src/http/app.ts createApp": { lines: 133, complexity: 26 }, "src/auth/flow-view.ts buildFlowView": { complexity: 24 }, "src/auth/hydra-admin.ts createHydraAdmin": { lines: 87 }, "src/auth/kratos-admin.ts createKratosAdmin": { lines: 68 }, diff --git a/src/http/app-i18n.test.ts b/src/http/app-i18n.test.ts index 6ed8e0b..24e57b6 100644 --- a/src/http/app-i18n.test.ts +++ b/src/http/app-i18n.test.ts @@ -78,12 +78,15 @@ test("a plugin that owns a landing page, or short-circuits a hook, translates fr mkdirSync(join(dir, "demo", "i18n"), { recursive: true }); t.after(() => rmSync(dir, { force: true, recursive: true })); writeFileSync(join(dir, "demo", "i18n", "en-US.ts"), 'const m = { "demo.hello": "Hello from the plugin" };\nexport default m;\n'); + mkdirSync(join(dir, "demo", "views"), { recursive: true }); + writeFileSync(join(dir, "demo", "views", "landing.ejs"), '<%= t("demo.hello") %>'); // Every plugin-owned render path: the public landing, the gated dashboard, and a hook short-circuit. + // The landings return a view, so the host's choice of views and context shows in the output. const demo: Plugin = { apiVersion: "1.0.0", - dashboard: (ctx) => ({ html: ctx.t("demo.hello") }), - home: (ctx) => ({ html: ctx.t("demo.hello") }), + dashboard: () => ({ view: "landing" }), + home: () => ({ view: "landing" }), hooks: { onRequest: (ctx) => (ctx.url.pathname === "/hooked" ? { html: ctx.t("demo.hello") } : undefined) }, id: "demo", }; @@ -100,6 +103,17 @@ test("a plugin that owns a landing page, or short-circuits a hook, translates fr assert.equal(await (await fetch(`${url}/dashboard`, { headers: { cookie } })).text(), "Hello from the plugin"); }); +test("a redirect from a plugin-owned landing keeps the visitor's chosen locale", async (t) => { + const demo: Plugin = { apiVersion: "1.0.0", home: () => ({ redirect: "/demo/next" }), id: "demo" }; + const app = createApp({ i18n: createI18n(await loadI18n()), plugins: [demo] }); + await new Promise((r) => app.listen(0, r)); + t.after(() => app.close()); + const url = `http://localhost:${(app.address() as AddressInfo).port}`; + const res = await fetch(`${url}/?locale=sv-SE`, { redirect: "manual" }); + assert.equal(res.status, 303); + assert.equal(res.headers.get("location"), "/demo/next?locale=sv-SE"); +}); + test("an error page renders without composing the menu — it exists for when the shell's data is what failed", async (t) => { // The chrome getter is lazy on purpose; a render that reads no chrome must not trigger it, or a // broken menu takes the error pages down with it. diff --git a/src/http/app.ts b/src/http/app.ts index 685e851..e7ba7d7 100644 --- a/src/http/app.ts +++ b/src/http/app.ts @@ -143,7 +143,6 @@ export function createApp(options: AppOptions = {}): Server { const serveHome = async (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory): Promise => { csrf.setCookie(); if (!homePlugin) return { data: { chrome: ctx.chrome, user: ctx.user }, view: "home" }; - // The plugin owns this page, so it runs on its own context — its catalog first, then core. const pluginCtx = contextFor(homePlugin.id); return { ctx: pluginCtx, pluginId: homePlugin.id, result: (await homePlugin.home(pluginCtx)) ?? null }; }; @@ -154,7 +153,7 @@ export function createApp(options: AppOptions = {}): Server { // The page carries the Sign-out form, so Set-Cookie a fresh CSRF token here when absent. csrf.setCookie(); if (!dashboardPlugin) return { data: { chrome: ctx.chrome }, view: "dashboard" }; - const pluginCtx = contextFor(dashboardPlugin.id); // as serveHome: the owner's own translator + const pluginCtx = contextFor(dashboardPlugin.id); return { ctx: pluginCtx, pluginId: dashboardPlugin.id, result: (await dashboardPlugin.dashboard(pluginCtx)) ?? null }; }; @@ -168,9 +167,8 @@ export function createApp(options: AppOptions = {}): Server { const host: Host = { anyRequestHooks, anyResponseHooks, assets, authOptions, builtinRoutes, csrfSecret, i18n, jwks, keto, kratos, kratosAdmin, menu, permissionCatalog, pluginViewsFor, plugins, secureCookies, settingsCatalog, system, viewsFor }; - // Static files, the canonical-host redirect and prepareRequest come first; then the answering - // stages in order — onRequest hook, plugin route, built-in route, else 404/405. A stage that - // returns a boolean returns true once it has answered the request, and false to pass it on. + // A stage that returns a boolean returns true once it has answered the request, and false to + // pass the request to the next stage. const handleRequest = async (req: IncomingMessage, res: ServerResponse, reqLog: Log): Promise => { // Error pages can render before this request has a context at all (a throw on the way to one), // so they start on the built-in English and switch to the visitor's locale once it is resolved. @@ -358,8 +356,7 @@ async function answerBuiltinRoute(host: Host, state: RequestState): Promise ({ handler: () => null, method, path }); +const entry = (method: "GET" | "POST", path: string): BuiltinRoute => ({ handler: () => ({ html: "" }), method, path }); test("matches exact path + method; GET also answers HEAD", () => { const get = entry("GET", "/error"); diff --git a/src/http/builtin-route.ts b/src/http/builtin-route.ts index 1abfbd8..7515a15 100644 --- a/src/http/builtin-route.ts +++ b/src/http/builtin-route.ts @@ -1,8 +1,6 @@ // The host's internal route table: each built-in endpoint (the auth/OAuth2 group, the landing // slots, /error) is a named handler with the plugin RouteHandler shape, plus the request's CSRF -// mint (host-only — a plugin reads the token via ctx.chrome instead). app.ts matches this table -// after plugin routes — exact path, a GET route also answering HEAD like the plugin router — and -// sends the result against the core views, or against the owning plugin's where it is a PluginOwned. +// mint (host-only — a plugin reads the token via ctx.chrome instead). import type { Gate } from "../auth/gate.ts"; import type { RequestContext } from "./context.ts"; import type { RouteResult } from "../plugin-host/plugin.ts"; @@ -20,8 +18,8 @@ export interface RequestCsrf { // own context — otherwise the plugin's keys render as bare keys on the pages it owns. export type PluginContextFactory = (pluginId: string) => RequestContext; -// A landing slot's result when a plugin owns it: the host renders it against that plugin's views -// on `ctx`, and shows it to the response hooks like a plugin route's result. +// What a landing slot returns when a plugin owns it. The host renders `result` with that plugin's +// views and `ctx`, and passes it to every plugin's onResponse hook, as it does for a plugin route. export interface PluginOwned { ctx: RequestContext; // the owner's own context, from PluginContextFactory pluginId: string; @@ -29,8 +27,7 @@ export interface PluginOwned { } export interface BuiltinRoute extends Gate { - // Returns a RouteResult, or null when the handler wrote to ctx.res itself. - handler: (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory) => Promise | RouteResult | PluginOwned | null; + handler: (ctx: RequestContext, csrf: RequestCsrf, contextFor: PluginContextFactory) => Promise | RouteResult | PluginOwned; method: "GET" | "POST"; // a GET route also answers HEAD, like plugin routes path: string; // exact pathname } -- 2.52.0 From 22b25bf3db84553e1b73aea62480886698d34ecf Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 00:51:27 +0100 Subject: [PATCH 6/7] =?UTF-8?q?Answer=20prose=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=205=20=F0=9F=9F=A1=20(0=20misread);=20comments=20?= =?UTF-8?q?fixed=20in=20the=20stability=20round's=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -- 2.52.0 From df9a3f3665e8e931cdbbeddc6868549d0dba59cf Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 00:55:21 +0100 Subject: [PATCH 7/7] Remove the shipped item 68 from the plan --- todo.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/todo.md b/todo.md index 2eacff0..2422d6a 100644 --- a/todo.md +++ b/todo.md @@ -12,7 +12,6 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| -| 68 | 0.5.0 | | | **Make `serveHome` and `serveDashboard` return their result to the request pipeline, as every other stage does, instead of sending it themselves.** | 2 | 1 | 3 | 4 | 3 | code health by 3 | 13.5 | | 66 | 0.5.0 | | | **Make `src/auth/bootstrap.ts` read its settings through `src/config.ts` and call Kratos and Keto through their client modules.** | 2 | 1 | 3 | 4 | 4 | code health by 3 | 9.0 | | 70 | 0.5.0 | | | **Give the duplicated paging loop, Link-header parser and `FieldConfig` type one home each, and rename `buildPluginChrome` to `buildPageChrome`.** | 2 | 1 | 1 | 2 | 3 | code health by 2 | 9.0 | | 71 | 0.5.0 | | | **Delete, with a group, every membership tuple in another group that names it, so re-creating the name restores no nested membership.** | 1 | 2 | 2 | 3 | 2 | 2 by 3 | 4.2 | @@ -29,15 +28,6 @@ ## Details -### 68. Make `serveHome` and `serveDashboard` return their result to the request pipeline, as every other stage does, instead of sending it themselves. - -In `src/http/app.ts`, both (~L143-169) call `sendResult` with the owning plugin's renderer and -return `null` when a plugin owns the slot, and return a `RouteResult` otherwise, so a reader of -`handleRequest` (~L183) and `prepareRequest` (~L274) meets two return conventions. Return the -result with whatever names its renderer and let the one send stage render it. Make -`prepareRequest` and `handleRequest` easier to read, places the seats found hard. The 19-field -`Host` bag is left alone: no checkable target for it came out of the scoring run. - ### 66. Make `src/auth/bootstrap.ts` read its settings through `src/config.ts` and call Kratos and Keto through their client modules. `seedAdminAndPermissions` (~L221-233) reads `ADMIN_EMAIL`, `ADMIN_PASSWORD`, `KETO_WRITE_URL`, -- 2.52.0