diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc38df..38d4b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ at boot. Entries start at 0.3.0. ## Unreleased +### The admin's recovery-code notice links to the recovery flow the code belongs to + +After an admin generates a recovery code for a user, the notice asks them to send the user both the +code and its link. The link opens the recovery flow the code belongs to, where the user enters it. +Before, the notice linked a new recovery flow at its email step, which doesn't accept the code. + ### A verification or recovery code pasted with whitespace around it is accepted The code field on the verification and recovery pages takes a code with spaces or tabs around it. diff --git a/docs/comprehension-history.md b/docs/comprehension-history.md index 93a263a..9e4e47f 100644 --- a/docs/comprehension-history.md +++ b/docs/comprehension-history.md @@ -106,3 +106,119 @@ Inherited architect, decided by: - Self-sufficiency: `e2e-tests/proxy.ts:14` `toKratos`: the predicate's contract is readable on its own line - Self-sufficiency: `e2e-tests/Dockerfile:11` `COPY tsconfig.json`: the relative `extends` path works in the checkout and in the image, and a comment names the link - Shape, Overall: `src/ui/icons.ts:4` `ICON_NAMES`: the registry matches its description, with no dead ids + +## 2026-10-31T12:28:23Z, PR #200 at 3592a30, against a8a9744 + +Ruling: same + +| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall | +|---|---|---|---|---|---| +| Mid A | better | better | better | same | better | +| Inherited architect | worse | worse | better | same | same | +| Maintainability senior | same | same | better | same | same | +| Junior A | better | better | better | better | better | + +Mid A, decided by: + +- Navigation, Locality, Shape: `src/ui/chrome.ts:74` `shellUser`: the deleted `buildShellContext` and `buildDashboardModel` left two shell models side by side, and now every page reads one `chrome` +- Locality, Shape: `src/auth/routes.ts:58` `flowFailure`: a named unit shared by `flowPage` and `codeFlowSubmit`, where the old `flowPage` held the whole error ladder inline +- Shape: `src/auth/kratos-public.ts:117` `submitFlow`: takes a flow type and id instead of an action URL, and the Referer quirk is explained where it applies +- Locality: `src/http/static.ts:93` `versioned`: reads the module-level `hashes` map, so an asset href depends on what an earlier request hashed +- Navigation, Shape: `src/ui/stylesheet-floor.ts:30` `floorViolations`: a 249-line regex walker that only tests use sits in `src/ui/` beside runtime modules, though the README now points straight at it + +Inherited architect, decided by: + +- Navigation: `public/css/styles.css:25` `:root`: the old comment next to the palette answered the 3am question and the new text drops it +- Navigation: `src/ui/stylesheet-floor.ts:1` `floorViolations`: its header never says an unsupported value in a custom property blanks every `var()` reading it, so a cold reader walks the regex walker to find the cause +- Navigation, Shape: `src/ui/stylesheet-floor.ts:12` `import`: test-only tooling that pulls a devDependency sits beside runtime modules and is missing from the README file map +- Locality: `src/http/static.ts:64` `hashes`: a module-level map shared by every `publicAssets()` instance makes a fresh href depend on request history elsewhere +- Locality: `src/http/app.ts:188` `renderPage`: `assetHref` is appended last at three render sites, and only a comment keeps a fourth from emitting unversioned hrefs +- Shape: `src/ui/chrome.ts:74` `shellUser`: removes the second shell model that duplicated `PageChrome` +- Shape: `src/auth/routes.ts:58` `flowFailure`: one named unit shared by `flowPage` and `codeFlowSubmit`, each handler reads in a screen + +Maintainability senior, decided by: + +- Shape, Locality: `src/ui/chrome.ts:74` `shellUser`: the deleted `ShellModel` left two shapes for the same chrome, and now the shell has one input and one builder +- Shape, Locality: `src/auth/routes.ts:58` `flowFailure`: the old `flowPage` mixed about 60 lines of error mapping with `return_to` handling, and now `flowReturnTo` and `flowFailure` read on their own +- Locality: `src/http/static.ts:64` `hashes`: a hidden module-level cache filled by two readers, and `assetHref` is merged by hand at three render sites +- Shape, Navigation: `src/ui/stylesheet-floor.ts:119` `positive`: a 249-line regex walker that only a test uses sits among runtime view modules + +Junior A, decided by: + +- Navigation, Self-sufficiency: `src/ui/stylesheet-floor.ts:1` `floorViolations`: the README's "Two browser floors" points here and the header lists what is checked, where the base had a prose rule and a hand-written regex test +- Shape, Locality: `src/ui/chrome.ts:74` `shellUser`: one shell view model replaces `ShellModel` beside `PageChrome`, and its helpers sit beside their only caller +- Shape, Locality: `src/auth/routes.ts:58` `flowFailure`: named units replace about 60 inline lines in `flowPage`, and `codeFlowSubmit` reuses them +- Self-sufficiency: `src/auth/kratos-admin.ts:68` `createRecoveryCode`: the comment no longer claims Kratos mails the link +- Locality: `src/http/static.ts:64` `hashes`: a module-level mutable map backs asset versioning, hidden state the base didn't have + +## 2026-10-31T12:30:19Z, PR #200 at 3592a30 + +| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall | +|---|---|---|---|---|---| +| Junior A | 7 | 6 | 6.5 | 6.5 | 6.5 | +| Mid A | 7 | 6 | 6.5 | 6.5 | 6.5 | +| Maintainability senior | 7 | 6 | 6.5 | 6.5 | 6.5 | +| Inherited architect | 7 | 5.5 | 5.5 | 7.5 | 6.5 | +| Mean | 7.00 | 5.88 | 6.25 | 6.75 | 6.50 | + +Junior A, hardest first: + +- Locality, Overall: `src/ui/stylesheet-floor.ts:119` `positive`: a regex-built `@supports` evaluator whose header says what it enforces but not how it decides a guard names a feature +- Locality, Overall: `src/http/app.ts:185` `handleRequest`: about 165 lines in one closure where each stage reads state an earlier stage set +- Self-sufficiency: `src/auth/routes.ts:58` `flowFailure`: its 4xx branches can't fire from `codeFlowSubmit` because `submitFlow` throws only on 5xx, and the call site doesn't say so + +Mid A, hardest first: + +- Locality, Overall: `src/ui/stylesheet-floor.ts:119` `positive`: a hand-written CSS walker with three regex passes per declaration, opaque at line level +- Locality, Overall: `src/http/app.ts:185` `handleRequest`: a fixed order of about 15 stages that nothing in the code enforces +- Self-sufficiency, Shape: `src/auth/login.ts:40` `readPermissions`: the comment names the tuple `#members` where the code uses `granted` +- Locality: `src/auth/routes.ts:121` `codeFlowSubmit`: one idea split across `flow-view.ts`, `routes.ts` and `kratos-public.ts` + +Maintainability senior, hardest first: + +- Locality, Overall: `src/http/app.ts:185` `handleRequest`: a 167-line closure of complexity 43 whose order across closure state nothing names as a contract +- Self-sufficiency, Locality: `src/auth/routes.ts:58` `flowFailure`: on the submit path every 4xx branch is dead and nothing at the call site says which can fire +- Locality: `src/ui/stylesheet-floor.ts:119` `positive`: the one module whose correctness can't be checked by reading +- Self-sufficiency: `src/auth/bootstrap.ts:31` `permissionTuple`: the comment says `#members` while the code writes `granted` + +Inherited architect, would restructure first: + +- Shape, Navigation: `src/auth/flow-view.ts:152` `formAction`: the flow-to-route mapping has two homes, here and `routes.ts`, so a code flow's route and its form target are decided in different files +- Shape, Navigation: `src/ui/stylesheet-floor.ts:30` `floorViolations`: a CSS linter that never runs at runtime sits among the runtime view helpers +- Locality, Shape: `src/http/app.ts:185` `handleRequest`: a level of its own inside `createApp` +- Shape: `src/http/builtin-routes.ts:23` `matchBuiltinRoute`: holds no routes, because the table is inline in `app.ts:176` and `buildAuthRoutes` + +## 2026-10-31T12:30:27Z, PR #200 rerun at 3592a30 + +| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall | +|---|---|---|---|---|---| +| Junior A | 7 | 5.5 | 6 | 6.5 | 6 | +| Mid A | 7 | 6 | 6.5 | 6.5 | 6.5 | +| Maintainability senior | 7 | 5.5 | 6 | 6.5 | 6 | +| Inherited architect | 7 | 6 | 6 | 6.5 | 6.5 | +| Mean | 7.00 | 5.75 | 6.13 | 6.50 | 6.25 | + +Junior A, hardest first: + +- Locality, Overall: `src/ui/stylesheet-floor.ts:119` `positive`: a regex-built CSS walker that stayed opaque without tracing it by hand +- Locality, Overall: `src/http/app.ts:185` `handleRequest`: about 320 lines of interdependent locals, with a fixed stage order that only a top-to-bottom read shows +- Self-sufficiency, Shape: `src/auth/login.ts:40` `completeLogin`: the header says `#members` where the code uses `granted` + +Mid A, hardest first: + +- Locality, Overall: `src/http/app.ts:185` `handleRequest`: about 165 lines of order-dependent stages, with `renderPage` reassigned partway +- Locality, Overall: `src/ui/stylesheet-floor.ts:119` `positive`: how it decides a guard names a feature stayed opaque +- Self-sufficiency, Shape: `src/auth/bootstrap.ts:31` `seedPermissions`: the comment says `#members` where the code uses `granted` + +Maintainability senior, hardest first: + +- Locality, Overall: `src/http/app.ts:185` `handleRequest`: about 167 lines and complexity 43 in a fixed order nobody states +- Locality, Overall: `src/ui/stylesheet-floor.ts:119` `positive`: a hand-rolled regex CSS walker, partly opaque +- Locality, Shape: `src/plugin-host/plugin.ts:126` `RESERVED_PLUGIN_IDS`: a hand-kept mirror of the host's path segments that has already drifted, as it omits `error` +- Self-sufficiency, Shape: `src/auth/login.ts:40` `readPermissions`: the comment says `#members` where the code uses `granted` + +Inherited architect, would restructure first: + +- Locality, Overall: `src/http/app.ts:185` `handleRequest`: the most concerns and the highest complexity, already grandfathered, and every request-wide concern lands in it +- Shape, Navigation: `src/ui/stylesheet-floor.ts:15` `FLOOR`: the 3am rule's home is a test-only module under `ui/` +- Shape: `src/auth/routes.ts:275` `buildAuthRoutes`: the 19-module `src/auth/` mixes six concerns, and this file with `flow-view.ts` is where the next flow lands diff --git a/docs/reading-cost.md b/docs/reading-cost.md index 69e6c51..46aa50b 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -257,3 +257,20 @@ Read most (opens and searches): - `README.md` 2 - `AGENTS.md` 1 - `src/ui/theme-switch.test.ts` 1 + +## Reading cost, chunk 4a258a237c53, finished 2026-10-31T12:04:07Z + +154777 new input tokens before the first commit. + +Read most (opens and searches): + +- `e2e-tests/full-flow.spec.ts` 9 +- `todo.md` 6 +- `README.md` 5 +- `src/auth/flow-view.ts` 5 +- `src/auth/flow-view.test.ts` 4 +- `src/auth/routes.ts` 4 +- `src/auth/kratos-public.test.ts` 3 +- `src/i18n/locales/en-US.ts` 3 +- `e2e-tests/compose.full.yml` 2 +- `src/auth/kratos-admin.ts` 2 diff --git a/e2e-tests/full-flow.spec.ts b/e2e-tests/full-flow.spec.ts index 3ae774e..3118329 100644 --- a/e2e-tests/full-flow.spec.ts +++ b/e2e-tests/full-flow.spec.ts @@ -138,6 +138,26 @@ test.describe.serial("authenticated admin journey", () => { await expect(page.locator("tr", { hasText: email })).toHaveCount(0); }); + // Kratos accepts an admin-minted code only in the flow it was minted on, which the notice's link opens. + test("recovery code: the notice's link opens the flow that accepts the code, and the code signs the user in", async () => { + test.setTimeout(90_000); + const email = `admin-recovery-${suffix}@plainpages.local`; + await createIdentity(email); + await page.goto("/admin/users"); + const editHref = await page.locator("tr", { hasText: email }).locator('a[href^="/admin/users/"]').first().getAttribute("href"); + await page.goto(editHref!); + await page.getByRole("button", { name: "Generate recovery code" }).click(); + const code = await page.locator(".recovery-code code").innerText(); + const link = await page.locator(".alert-body a").getAttribute("href"); + + const user = await watchedPage(await browser.newContext()); + await user.goto(link!); + await user.fill('input[name="code"]', code); + await user.getByRole("button", { name: "Continue" }).click(); + await expect(user).toHaveURL(/\/settings/); + await user.context().close(); + }); + test("groups CRUD: create a group (writes go to Keto), see it listed, then grant it a permission", async ({ browser }) => { // A Keto set exists only while it has ≥1 member, so create needs a first member (the form // enforces it); pick the first option (a user) from the required picker. diff --git a/examples/plugins/admin/i18n/en-US.ts b/examples/plugins/admin/i18n/en-US.ts index 6cb414d..b3bad1f 100644 --- a/examples/plugins/admin/i18n/en-US.ts +++ b/examples/plugins/admin/i18n/en-US.ts @@ -141,8 +141,8 @@ const messages = { "admin.users.new": "New user", "admin.users.pagination": "Users pagination", "admin.users.reactivate": "Reactivate", - "admin.users.recovery.body": "Give it to the user — they enter it to set a new password (generate a fresh one if it has expired):", - "admin.users.recovery.link": "the password-reset screen", + "admin.users.recovery.body": "Send the user this link and code. They open the link and enter the code to set a new password (generate a fresh code if it has expired):", + "admin.users.recovery.link": "password-reset link", "admin.users.recovery.generate": "Generate recovery code", "admin.users.recovery.title": "Recovery code generated", "admin.users.save": "Save changes", diff --git a/examples/plugins/admin/i18n/sv-SE.ts b/examples/plugins/admin/i18n/sv-SE.ts index 00c6824..1dec870 100644 --- a/examples/plugins/admin/i18n/sv-SE.ts +++ b/examples/plugins/admin/i18n/sv-SE.ts @@ -141,8 +141,8 @@ const messages: AdminMessages = { "admin.users.new": "Ny användare", "admin.users.pagination": "Sidnavigering för användare", "admin.users.reactivate": "Aktivera igen", - "admin.users.recovery.body": "Ge den till användaren — koden anges för att sätta ett nytt lösenord (skapa en ny om den hunnit gå ut):", - "admin.users.recovery.link": "sidan för lösenordsåterställning", + "admin.users.recovery.body": "Skicka länken och koden till användaren. Användaren öppnar länken och anger koden för att sätta ett nytt lösenord (skapa en ny kod om den hunnit gå ut):", + "admin.users.recovery.link": "länk för lösenordsåterställning", "admin.users.recovery.generate": "Skapa återställningskod", "admin.users.recovery.title": "Återställningskod skapad", "admin.users.save": "Spara ändringar", diff --git a/examples/plugins/admin/views/partials/user-form-body.ejs b/examples/plugins/admin/views/partials/user-form-body.ejs index a9c3d78..bf8188b 100644 --- a/examples/plugins/admin/views/partials/user-form-body.ejs +++ b/examples/plugins/admin/views/partials/user-form-body.ejs @@ -2,7 +2,7 @@ Admin user create/edit form body, captured into the shell content slot. Config: form { action, csrfToken, submitLabel, cancelHref, fields: field.ejs config[] } edit? { nextLabel, stateAction, recoveryAction, deleteAction } (edit mode only) - recovery? { code? } shown after a recovery code is generated (recovery is code-based) + recovery? { code, link } shown after a recovery code is generated; link is the URL of the flow the code was minted on, the only flow that accepts the code error? string shown when a write was rejected %><% const form = locals.form; @@ -14,7 +14,7 @@ <%- include("partials/alert", { text: locals.error, tone: "neg" }) %> <% } -%> <% if (recovery) { -%> -
<%= t("admin.users.recovery.title") %><%= t("admin.users.recovery.body") %> "><%= t("admin.users.recovery.link") %><% if (recovery.code) { %><%= recovery.code %><% } %>
+
<%= t("admin.users.recovery.title") %><%= t("admin.users.recovery.body") %> <%= t("admin.users.recovery.link") %><%= recovery.code %>
<% } -%>
diff --git a/src/auth/kratos-admin.test.ts b/src/auth/kratos-admin.test.ts index 69a7278..b9c6d4f 100644 --- a/src/auth/kratos-admin.test.ts +++ b/src/auth/kratos-admin.test.ts @@ -100,9 +100,9 @@ test("updateMetadataPublic PATCHes a JSON-Patch `add /metadata_public` so it nev }); test("createRecoveryCode POSTs the identity id to /admin/recovery/code → { code, link }", async () => { - const { calls, fetchImpl } = recorder(() => res(200, { recovery_code: "123456", recovery_link: "http://kratos/self-service/recovery?flow=f&code=123456" })); + const { calls, fetchImpl } = recorder(() => res(200, { recovery_code: "123456", recovery_link: "http://kratos/self-service/recovery?flow=f" })); const out = await createKratosAdmin({ baseUrl: BASE, fetchImpl }).createRecoveryCode(ID); - assert.deepEqual(out, { code: "123456", link: "http://kratos/self-service/recovery?flow=f&code=123456" }); + assert.deepEqual(out, { code: "123456", link: "http://kratos/self-service/recovery?flow=f" }); assert.equal(calls[0]!.method, "POST"); assert.match(calls[0]!.url, /\/admin\/recovery\/code$/); assert.deepEqual(JSON.parse(calls[0]!.body!), { identity_id: ID }); diff --git a/src/auth/kratos-admin.ts b/src/auth/kratos-admin.ts index 1ef631a..52c67dc 100644 --- a/src/auth/kratos-admin.ts +++ b/src/auth/kratos-admin.ts @@ -25,7 +25,7 @@ export interface ListOptions { pageToken?: string; } -// A one-time recovery code + the self-service link wrapping it (admin "trigger recovery"). +// A one-time recovery code + the URL of the recovery flow it was minted on, the only flow that accepts it. export interface RecoveryCode { code: string; link: string; @@ -65,8 +65,8 @@ export function createKratosAdmin(config: { baseUrl: string; fetchImpl?: typeof return (await res.json()) as Identity; }, - // Mint a recovery code for an identity (admin "trigger recovery") — the link is mailed to the - // user by Kratos; the code/link are also returned so an operator can hand them over directly. + // Mint a recovery code for an identity (admin "trigger recovery"). Kratos mails nothing, so the + // operator hands the code and link to the user. async createRecoveryCode(userId, opts = {}) { const body: Record = { identity_id: userId }; if (opts.expiresIn) body.expires_in = opts.expiresIn; diff --git a/src/http/app-admin.test.ts b/src/http/app-admin.test.ts index dcf1c4b..e54f843 100644 --- a/src/http/app-admin.test.ts +++ b/src/http/app-admin.test.ts @@ -63,7 +63,7 @@ test("admin Users screen: gate, list/filter, create, edit, deactivate, delete, r let lastCreate: { traits?: unknown } | undefined; const kratosAdmin = stubAdmin({ createIdentity: async (payload) => { lastCreate = payload as { traits?: unknown }; const created = mk("grace@example.com"); store.push(created); return created; }, - createRecoveryCode: async (id) => ({ code: "123456", link: `http://kratos/self-service/recovery?code=123456&id=${id}` }), + createRecoveryCode: async (id) => ({ code: "123456", link: `http://kratos/self-service/recovery?flow=flow-${id}` }), deleteIdentity: async (id) => { const i = store.findIndex((x) => x.id === id); if (i >= 0) store.splice(i, 1); }, getIdentity: async (id) => store.find((x) => x.id === id) ?? null, listIdentities: async () => ({ identities: store, nextPageToken: null }), @@ -133,13 +133,13 @@ test("admin Users screen: gate, list/filter, create, edit, deactivate, delete, r assert.equal(target.state, "inactive"); assert.equal(denylist.isRevoked(target.id, 0), true); - // Recovery: renders the edit page (200) showing the generated code (code-based; no admin-host link). + // Recovery: renders the edit page (200) showing the generated code, linked to the flow it was minted on. const rec = await post(`/admin/users/${target.id}/recovery`, `_csrf=${token}`); assert.equal(rec.status, 200); const recHtml = await rec.text(); assert.match(recHtml, /Recovery code generated/); assert.match(recHtml, /123456<\/code>/); - assert.doesNotMatch(recHtml, /self-service\/recovery\?code=/); // the unreachable admin-API link is gone + assert.match(recHtml, new RegExp(``)); // Delete needs a deliberate confirm step (zero-JS): GET renders the interstitial, POST performs it. const confirm = await (await get(`/admin/users/${target.id}/delete`)).text(); diff --git a/todo.md b/todo.md index 9c1dcb5..eabe39d 100644 --- a/todo.md +++ b/todo.md @@ -6,19 +6,27 @@ `Bar = 1` -`Next ID = 46` +`Next ID = 55` ## Items | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| -| 45 | 0.4.21 | | | **Link the admin's recovery-code notice to the flow the code was minted on, so the user can enter it.** | 1 | 2 | 2 | 3 | 2 | 2 by 5 | 7.0 | +| 46 | 0.4.21 | | security | **Keep a secret setting's raw value out of the boot error that rejects it.** | 1 | 1 | 4 | 5 | 2 | | 0.0 | | 19 | 0.5.0 | | question | **Run the documented Docker commands on macOS and fix whatever misbehaves.** | 2 | 2 | 1 | 3 | 3 | 1 by 3 | 3.3 | | 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 | +| 48 | 0.5.0 | | | **Correct the Keto tuple in the `login.ts` and `bootstrap.ts` comments to the `granted` relation the code writes.** | 1 | 1 | 1 | 1 | 1 | code health by 4 | 28.7 | +| 51 | 0.5.0 | | | **Move the test-only tooling out of the runtime folders `src/ui/` and `src/http/`, and keep the README file map true.** | 2 | 1 | 1 | 2 | 3 | code health by 4 | 18.0 | +| 52 | 0.5.0 | 51 | | **Make `positive` in `stylesheet-floor.ts` readable without tracing it by hand, and state why a custom property's value is checked.** | 2 | 1 | 1 | 2 | 3 | code health by 5 | 22.5 | +| 53 | 0.5.0 | | | **Say at `codeFlowSubmit` which branches of `flowFailure` can fire there.** | 1 | 1 | 1 | 1 | 1 | code health by 3 | 21.6 | +| 49 | 0.5.0 | | | **Render the admin's "group already exists" refusal from the catalog, in every locale.** | 1 | 2 | 1 | 2 | 1 | code health by 2 | 14.4 | | 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 | | 43 | 0.5.0 | | | **Let a plugin version the scripts and other `/public/` files its own markup links, so they are cached as immutable too.** | 2 | 2 | 1 | 3 | 3 | 8 by 3, 7 by 2 | 5.0 | +| 47 | 0.5.0 | | | **Refuse the plugin id `error`, and make the drift guard cover the `/error` mount.** | 1 | 1 | 2 | 2 | 1 | 7 by 3 | 4.9 | | 28 | 0.5.0 | | | **Let a plugin add entries to the profile menu through the plugin API.** | 2 | 2 | 2 | 4 | 4 | 7 by 4, 1 by 2 | 4.2 | +| 50 | 0.5.0 | | | **Split `handleRequest` into named stages, so each fits the 50-line limit and the stage order is written once.** | 3 | 1 | 3 | 5 | 7 | code health by 7 | 3.7 | +| 54 | 0.5.0 | | | **List the `setting` conflict kind in the README's Conflict rules table.** | 1 | 1 | 1 | 1 | 1 | 9 by 2 | 3.1 | | 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 | | 41 | 0.5.0 | | | **Apply a permission save as one Keto call, so it cannot stop part-way.** | 2 | 2 | 2 | 4 | 3 | 2 by 2 | 2.1 | @@ -26,12 +34,12 @@ ## Details -### 45. Link the admin's recovery-code notice to the flow the code was minted on, so the user can enter it. +### 46. Keep a secret setting's raw value out of the boot error that rejects it. -The notice (`examples/plugins/admin/views/partials/user-form-body.ejs`) links a bare `/recovery`, -which starts a fresh flow at its email step. Kratos binds an admin-minted code to the flow it was minted on, -whose URL is the `recovery_link` that `createRecoveryCode` returns as `RecoveryCode.link`. Render that -link instead. +`coerce` in `src/plugin-host/settings.ts` puts `got "${raw}"` (boolean, enum, number) and `: ${raw}` +(url) into the error it returns, whatever `secret` says. A secret of type `url`, such as a DSN with +a password, that fails `new URL` reaches the boot error and so the log. Name the variable and what it +accepts, never the value; test one secret of each type. ### 19. Run the documented Docker commands on macOS and fix whatever misbehaves. @@ -43,6 +51,36 @@ Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id - 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. +### 48. Correct the Keto tuple in the `login.ts` and `bootstrap.ts` comments to the `granted` relation the code writes. + +`login.ts:40` and `bootstrap.ts:31` write `Permission:#members@user:`; the code, the OPL +file and the README use `granted`, and `members` is the Group relation. Raised by every comprehension +seat that read them. + +### 51. Move the test-only tooling out of the runtime folders `src/ui/` and `src/http/`, and keep the README file map true. + +`src/ui/stylesheet-floor.ts` imports a devDependency and only its test uses it; `src/http/app-harness.ts` +is a test fixture. Both sit beside runtime modules, and the README file map omits them. Seats also +named `src/http/builtin-routes.ts`, which holds no routes. + +### 52. Make `positive` in `stylesheet-floor.ts` readable without tracing it by hand, and state why a custom property's value is checked. + +Most scoring seats named `positive` (`:119`) with `walk` and `property` as opaque. Split it +into named steps, say in the header that an unsupported value in a custom property blanks every +`var()` that reads it, and keep the README's "Two browser floors" pointing here. It is the home of +the 3am answer. + +### 53. Say at `codeFlowSubmit` which branches of `flowFailure` can fire there. + +`submitFlow` throws only on a 5xx, so on the submit path only the 503 branch can fire; three seats +found that by opening a second file. + +### 49. Render the admin's "group already exists" refusal from the catalog, in every locale. + +`examples/plugins/admin/admin-groups.ts` returns the English string "A group with that name already +exists." where AGENTS.md puts every user-visible string in a catalog (`admin/i18n/en-US.ts`, then +`sv-SE.ts`). + ### 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 @@ -62,12 +100,31 @@ favicon and logo, and a plugin's `styles`. A file a plugin's own markup links, s the plugin API, hence a minor. Pick one with the plugin authors' panel, then document it in README → Reserved view locals. +### 47. Refuse the plugin id `error`, and make the drift guard cover the `/error` mount. + +`/error` is Kratos' error sink (`src/auth/routes.ts`), and plugin routes resolve before built-ins, so +a plugin with id `error` and a `/` route shadows it. Add `error` to `RESERVED_PLUGIN_IDS` and to the +mount list in `plugin.test.ts`'s drift guard, whose comment claims every built-in is derived. + ### 28. Let a plugin add entries to the profile menu through the plugin API. The profile dropdown stays a dropdown: Sign out is its only entry today, and it will gain more. `views/partials/shell.ejs` hand-rolls it (AGENTS.md → UI, the dropdown rule), so the entries need a manifest field that discovery checks, gated like a nav node. +### 50. Split `handleRequest` into named stages, so each fits the 50-line limit and the stage order is written once. + +The hardest place in all eight scoring seats of PR #200: `src/http/app.ts` `handleRequest` is about +165 lines at complexity 43, in `createApp`'s closure, and the order of its stages (headers, static, +canonical host, locale, session, CSRF, contexts, hooks, dispatch) is carried by comments only. Remove +it from `grandfathered` in `src/function-limits.test.ts`. Behaviour must not change; `app-*.test.ts` +is the net. Ships alone (R 5). + +### 54. List the `setting` conflict kind in the README's Conflict rules table. + +`findConflicts` (`plugin.ts`) emits `kind: "setting"` at `error` level and the table in README → +Conflict rules omits it. + ### 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,