Link the admin's recovery-code notice to the flow the code was minted on #200
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) { -%>
|
||||
<div class="alert alert-pos" role="status"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-check-circle"/></svg><div class="alert-body"><strong><%= t("admin.users.recovery.title") %></strong><span><%= t("admin.users.recovery.body") %> <a href="<%= localeHref("/recovery") %>"><%= t("admin.users.recovery.link") %></a></span><% if (recovery.code) { %><span class="recovery-code"><code><%= recovery.code %></code></span><% } %></div></div>
|
||||
<div class="alert alert-pos" role="status"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-check-circle"/></svg><div class="alert-body"><strong><%= t("admin.users.recovery.title") %></strong><span><%= t("admin.users.recovery.body") %> <a href="<%= recovery.link %>"><%= t("admin.users.recovery.link") %></a></span><span class="recovery-code"><code><%= recovery.code %></code></span></div></div>
|
||||
<% } -%>
|
||||
<form class="form-card" method="post" action="<%= localeHref(form.action) %>">
|
||||
<input type="hidden" name="_csrf" value="<%= form.csrfToken %>">
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<string, unknown> = { identity_id: userId };
|
||||
if (opts.expiresIn) body.expires_in = opts.expiresIn;
|
||||
|
||||
@@ -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, /<code>123456<\/code>/);
|
||||
assert.doesNotMatch(recHtml, /self-service\/recovery\?code=/); // the unreachable admin-API link is gone
|
||||
assert.match(recHtml, new RegExp(`<a href="http://kratos/self-service/recovery\\?flow=flow-${target.id}">`));
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -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:<name>#members@user:<id>`; 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,
|
||||
|
||||
Reference in New Issue
Block a user