Compare commits

...

12 Commits

Author SHA1 Message Date
renovate-bot 254b512020 Update dependency lucide-static to v1.67.0
CI / full-gate (push) Successful in 3m33s
Mirror / github-mirror (push) Successful in 3s
Release / retag-image (push) Successful in 17s
Release / publish-overview (push) Successful in 3s
Release-Bump: minor
2026-11-04 04:18:37 +00:00
lilleman 529dd5cfa1 Mark 0.4.21 and 0.4.22 renamed to 0.5.0
CI / full-gate (push) Successful in 3m27s
Mirror / github-mirror (push) Successful in 5s
2026-11-03 21:55:15 +01:00
lilleman 95a9f28a5f Answer stability round 2: 0 🔴, 0 🟡 (0 misread); the order-free stylesheet comparison holds
CI / full-gate (push) Successful in 3m26s
2026-11-03 21:50:47 +01:00
lilleman 29fac748f9 Compare the repeat view's stylesheets in any order, since WebKit finishes cache hits in no set order 2026-11-03 21:50:13 +01:00
lilleman 7100740eb5 Answer stability round 1: 0 🔴, 1 🟡 (0 misread); the same reserved-ids fix
CI / full-gate (push) Failing after 1m19s
2026-11-03 21:46:57 +01:00
lilleman 0cd5d368da Answer product-owner round 1: 0 🔴, 1 🟡 (0 misread); the README's reserved ids list error, fixed in the prose round's commit 2026-11-03 21:46:57 +01:00
lilleman a3b8632d5c Answer prose round 1: 0 🔴, 1 🟡 (0 misread); the secrets checklist reads in one pass, and four comments shrink or say granted 2026-11-03 21:46:57 +01:00
lilleman 69f0371b89 Answer security round 1: 0 🔴, 0 🟡 (0 misread); /security-review found nothing 2026-11-03 21:46:57 +01:00
lilleman 190d12532d Record the reading cost
CI / full-gate (push) Failing after 1m4s
2026-11-03 21:44:05 +01:00
lilleman fefc7cee30 Plan 0.5.0 from the ten highest-scoring items; reserve the plugin id error, translate the duplicate-group refusal, and correct four comments and README passages 2026-11-03 21:43:57 +01:00
renovate-bot 0ebccfd1b6 Update dependency @mdn/browser-compat-data to v8.1.8
CI / full-gate (push) Successful in 5m44s
Mirror / github-mirror (push) Successful in 4s
2026-11-03 04:19:00 +00:00
renovate-bot d1fb444e51 Update renovate/renovate Docker tag to v44.199.2
CI / full-gate (push) Successful in 4m7s
Mirror / github-mirror (push) Successful in 5s
2026-11-02 04:19:45 +00:00
17 changed files with 83 additions and 86 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
-e RENOVATE_PLATFORM=gitea \
-e RENOVATE_REPOSITORIES=${{ github.repository }} \
-e RENOVATE_TOKEN \
renovate/renovate:44.186.6
renovate/renovate:44.199.2
# 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
+10
View File
@@ -6,6 +6,16 @@ at boot. Entries start at 0.3.0.
## Unreleased
### Discovery refuses a plugin with the id `error`
`/error` is the page Kratos sends a failed self-service flow to, and a plugin named `error` would
shadow it. Discovery now refuses that id at boot, like the other reserved ids. Rename such a plugin's
folder before upgrading.
### The admin's "group already exists" refusal is translated
Creating a group whose name is taken shows the refusal in the page's language, not always in English.
### A malformed URL variable's boot error no longer prints the value
When a URL variable such as `PLUGIN_DB_URL`, `PLUGIN_DB_ADMIN_URL` or `OTLP_ENDPOINT` can't be parsed,
+6 -6
View File
@@ -417,7 +417,7 @@ URL/path-safe (`isValidPluginId`: lowercase `a–z`, digits, dashes) and also na
`views/`, its `/public/<id>/` assets, and by convention its nav/permission names.
`RESERVED_PLUGIN_IDS` are refused at discovery — the gated `dashboard`, the Kratos auth flows
(`auth`, `login`, `logout`, `recovery`, `registration`, `settings`, `verification`), the `oauth2`
(`auth`, `login`, `logout`, `recovery`, `registration`, `settings`, `verification`) and their `error` page, the `oauth2`
provider routes, and `public` — since plugin routes resolve first and a folder claiming one would
silently shadow a built-in. `admin` is **not** reserved: the admin screens are themselves a plugin.
@@ -1271,9 +1271,9 @@ override sets `APP_URL=http://localhost:3000`, and `127.0.0.1` is canonicalised
A clean clone needs **none** of the above. What can't be auto-generated is **production-only**:
1. **Production secrets** — every value below ships as a committed dev throwaway and **must** be
replaced before a deploy faces the internet. Only the first is enforced:
`REQUIRE_SECURE_SECRETS=true` refuses to boot on a missing or throwaway `CSRF_SECRET` and
**nothing else** — the rest fail silently, so treat this as a checklist.
replaced before a deploy faces the internet. Of them, `REQUIRE_SECURE_SECRETS=true` checks only
`CSRF_SECRET`, and the flag's [Configuration](#configuration) row lists what else it checks; the
rest fail silently, so treat the table as a checklist.
| Secret | Where | Protects |
| --- | --- | --- |
@@ -1439,8 +1439,8 @@ unauthenticated Ory ports. Install only what you trust, and let the plugin's own
Hardening a real deploy is `REQUIRE_SECURE_SECRETS=true`, `SECURE_COOKIES=true`, and replacing
**every** committed dev secret ([what you must supply](#what-you-must-supply-the-only-manual-prep)).
`REQUIRE_SECURE_SECRETS` guards only `CSRF_SECRET`; nothing fails loud if you ship Ory's, Postgres'
or the demo admin's throwaways.
`REQUIRE_SECURE_SECRETS` guards only the secrets its [Configuration](#configuration) row lists;
nothing fails loud if you ship Ory's, Postgres' or the demo admin's throwaways.
## Email
+16
View File
@@ -304,3 +304,19 @@ Read most (opens and searches):
- `CHANGELOG.md` 2
- `src/plugin-host/settings.ts` 2
- `AGENTS.md` 1
## Reading cost, chunk 1a31ef102e70, finished 2026-10-31T13:14:10Z
70498 new input tokens before the first commit.
Read most (opens and searches):
- `README.md` 4
- `src/auth/bootstrap.test.ts` 4
- `src/auth/bootstrap.ts` 4
- `todo.md` 4
- `CHANGELOG.md` 1
- `src/config.test.ts` 1
- `src/config.ts` 1
- `src/plugin-host/storage-provisioning.ts` 1
- `src/plugin-host/storage.ts` 1
+3 -1
View File
@@ -264,7 +264,9 @@ test("a repeat page view fetches no stylesheet again", async ({ page }) => {
await page.goto("/");
const repeat = await stylesheets();
expect(repeat.map(({ name }) => name)).toEqual(first.map(({ name }) => name));
// A cache hit completes in no set order, so the two views list the same files in any order.
const names = (entries: { name: string }[]): string[] => entries.map(({ name }) => name).sort();
expect(names(repeat)).toEqual(names(first));
for (const { name, transferSize } of repeat) expect(transferSize, `${name} on the repeat view`).toBe(0);
});
+1 -1
View File
@@ -364,7 +364,7 @@ export const groupsCreate = withGroups(async (deps) => {
const reject = async (error: string): Promise<RouteResult> => ({ ...(await groupFormResult(deps, { error, values: { member, name } })), status: 400 });
if (!isValidGroupName(name)) return reject(ctx.t("admin.groups.validation.name"));
if (!tuple) return reject(ctx.t("admin.groups.validation.member"));
if (await groupExists(keto, name)) return reject("A group with that name already exists.");
if (await groupExists(keto, name)) return reject(ctx.t("admin.groups.error.duplicate"));
await keto.writeTuple(tuple);
ctx.log.info("admin: group created", { actor: user.id, group: name });
return { redirect: detailHref(name) };
+1
View File
@@ -68,6 +68,7 @@ const messages = {
"admin.groups.create": "Create group",
"admin.groups.delete": "Delete group",
"admin.groups.deleteMessage": "Delete group {{name}}? This can't be undone.",
"admin.groups.error.duplicate": "A group with that name already exists.",
"admin.groups.error.selfLockout": "This change would lock you out of {{permissions}}, which you hold through this group. Ask another administrator to make it.",
"admin.groups.field.name": "Group name",
"admin.groups.field.nameHint": "Lowercase letters, digits, dashes and underscores.",
+1
View File
@@ -68,6 +68,7 @@ const messages: AdminMessages = {
"admin.groups.create": "Skapa grupp",
"admin.groups.delete": "Radera grupp",
"admin.groups.deleteMessage": "Ta bort gruppen {{name}}? Det går inte att ångra.",
"admin.groups.error.duplicate": "Det finns redan en grupp med det namnet.",
"admin.groups.error.selfLockout": "Ändringen skulle låsa ute dig från {{permissions}}, som du har genom den här gruppen. Be en annan administratör att göra den.",
"admin.groups.field.name": "Gruppnamn",
"admin.groups.field.nameHint": "Små bokstäver, siffror, bindestreck och understreck.",
+8 -8
View File
@@ -8,11 +8,11 @@
"dependencies": {
"@larvit/log": "2.3.0",
"ejs": "6.0.1",
"lucide-static": "1.66.0",
"lucide-static": "1.67.0",
"postgres": "3.4.9"
},
"devDependencies": {
"@mdn/browser-compat-data": "8.1.7",
"@mdn/browser-compat-data": "8.1.8",
"@types/ejs": "3.1.5",
"@types/node": "24.19.2",
"acorn": "8.19.0",
@@ -32,9 +32,9 @@
}
},
"node_modules/@mdn/browser-compat-data": {
"version": "8.1.7",
"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-8.1.7.tgz",
"integrity": "sha512-TJ+rX530bFoE8678PuH2gjCSzwpBcaLlcElRhnnxFdkLHHT1clUAxtjImPFZDAlWQ005px9xaJ4Nd+aQp/rylQ==",
"version": "8.1.8",
"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-8.1.8.tgz",
"integrity": "sha512-+RK7inkLeZ70kr9Mo+Y81JjZMpQIt0Sx40tHHU0hXGDSdf9YvlawH+4WlUpP3SE8rFLmjqGiTzL+qPPPnncWDw==",
"dev": true,
"license": "CC0-1.0"
},
@@ -421,9 +421,9 @@
}
},
"node_modules/lucide-static": {
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.66.0.tgz",
"integrity": "sha512-j8Bjf/f21PtlD8p0wx1lRxyOrs98WZ1QEImkWmfskuJ8mOCwTN7Dwc6EHUzJDSzh79aB+Bo7SWcoTCM2zEjnJg==",
"version": "1.67.0",
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.67.0.tgz",
"integrity": "sha512-Lu/Ntwfw1GOQiNgTC2iHd/PC4QbqzwktLVFHTB84tldlfUjFgQ+EBWiZc/UPrlNdX97O/KXWRn792hIdQh6nhA==",
"license": "ISC"
},
"node_modules/postgres": {
+2 -2
View File
@@ -18,11 +18,11 @@
"dependencies": {
"@larvit/log": "2.3.0",
"ejs": "6.0.1",
"lucide-static": "1.66.0",
"lucide-static": "1.67.0",
"postgres": "3.4.9"
},
"devDependencies": {
"@mdn/browser-compat-data": "8.1.7",
"@mdn/browser-compat-data": "8.1.8",
"@types/ejs": "3.1.5",
"@types/node": "24.19.2",
"acorn": "8.19.0",
-2
View File
@@ -28,8 +28,6 @@ export function identityPayload(email: string, password: string) {
};
}
// Coarse-permission grant: `Permission:<permission>#members@user:<id>`. Subject ids are `user:<kratos-id>`
// (namespaces.keto.ts) — the source of truth the login flow projects into the JWT permissions.
export function permissionTuple(userId: string, permission: string) {
return { namespace: "Permission", object: permission, relation: "granted", subject_id: `user:${userId}` };
}
+3 -3
View File
@@ -37,9 +37,9 @@ export interface CompletedLogin {
permissions: string[];
}
// The coarse permissions a user holds — directly (`Permission:<name>#members@user:<id>`) or transitively via a
// group that is a member of the permission. Enumerates the defined permissions (the distinct objects in the Permission
// namespace) and asks Keto to resolve each membership, so a permission granted to a group reaches the JWT —
// The coarse permissions a user holds — directly (`Permission:<name>#granted@user:<id>`) or transitively via a
// group granted the permission. Enumerates the defined permissions (the distinct objects in the Permission
// namespace) and asks Keto to check each grant, so a permission granted to a group reaches the JWT —
// matching the OPL model and the admin "Effective access" view. At login/refresh only, never per
// request; permission count is small, so the per-permission checks are cheap and run in parallel.
export async function readPermissions(keto: KetoClient, userId: string): Promise<string[]> {
+2 -1
View File
@@ -117,7 +117,8 @@ function flowPage(kratos: KratosPublic, flowType: FlowType, secureCookies: boole
// A code flow's POST (flow-view.ts → CODE_FLOWS). Trims the pasted one-time code, since Kratos
// refuses one with whitespace around it, then passes the form on to Kratos and sends the browser
// where Kratos redirects. Kratos' csrf_token field guards the POST, so the host's own CSRF token
// isn't checked here.
// isn't checked here. `submitFlow` throws only on a 5xx or an unreachable Kratos, so only
// `flowFailure`'s 503 branch can fire here.
function codeFlowSubmit(kratos: KratosPublic, flowType: FlowType, secureCookies: boolean): BuiltinRoute["handler"] {
return async (ctx: RequestContext): Promise<RouteResult> => {
const pathname = ctx.url.pathname;
+3 -1
View File
@@ -191,7 +191,9 @@ test("admin Groups screen: gate, list, create, detail/membership, delete (CSRF-g
// An invalid name, a duplicate name, or a missing CSRF token are all refused, nothing written.
const before = tuples.length;
assert.equal((await post("/admin/groups", `_csrf=${token}&name=Bad Name&member=user:${grace}`)).status, 400);
assert.equal((await post("/admin/groups", `_csrf=${token}&name=eng&member=user:${grace}`)).status, 400); // already exists
const duplicate = await post("/admin/groups?locale=sv-SE", `_csrf=${token}&name=eng&member=user:${grace}`);
assert.equal(duplicate.status, 400);
assert.match(await duplicate.text(), /Det finns redan en grupp med det namnet\./);
assert.equal((await post("/admin/groups", `name=x&member=user:${grace}`)).status, 403);
assert.equal(tuples.length, before);
+3 -3
View File
@@ -139,9 +139,8 @@ test("findConflicts: each single slot (`home`/`dashboard`) may have one owner
assert.deepEqual(findConflicts([p({ id: "a", dashboard: handler, home: handler }), p({ id: "b" })]).filter((c) => c.kind === "home" || c.kind === "dashboard"), []);
});
// Drift guard: RESERVED_PLUGIN_IDS is a hand-maintained mirror of the host's own top-level mounts —
// a folder claiming one would silently shadow a built-in route. Derive the segments from the real
// route constants so adding a new auth flow or provider route without reserving its id fails here.
// Drift guard: every mount but the auth flows is listed by hand, so a new top-level built-in route
// needs a line here too.
test("RESERVED_PLUGIN_IDS covers every built-in top-level mount; `home` (the / field) and `admin` (a plugin) are NOT reserved", () => {
const seg = (path: string): string => path.split("/")[1] ?? ""; // first segment of "/x/y"
const builtins = new Set<string>([
@@ -151,6 +150,7 @@ test("RESERVED_PLUGIN_IDS covers every built-in top-level mount; `home` (the / f
"oauth2", // /oauth2/login · /consent · /logout (Hydra provider)
"dashboard", // the gated app home
"public", // static assets
"error", // /error (Kratos' error sink)
]);
for (const id of builtins) assert.ok(RESERVED_PLUGIN_IDS.has(id), `built-in mount "${id}" must be a reserved plugin id`);
// "/" is owned by the `home` manifest field (not a /<id> route), so it cannot be shadowed and is
+1 -1
View File
@@ -124,7 +124,7 @@ export function isValidPluginId(id: string): boolean {
// one — discovery refuses it. "/" is owned by the `home` field, not a route, so it needs no
// reservation; `admin` is deliberately absent, the admin screens being a drop-in plugin.
export const RESERVED_PLUGIN_IDS: ReadonlySet<string> = new Set([
"auth", "dashboard", "login", "logout", "oauth2", "public", "recovery", "registration", "settings", "verification",
"auth", "dashboard", "error", "login", "logout", "oauth2", "public", "recovery", "registration", "settings", "verification",
]);
export interface Semver {
+22 -56
View File
@@ -12,55 +12,28 @@
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 56 | 0.4.22 | | | **Correct the README's two statements that `REQUIRE_SECURE_SECRETS` guards only `CSRF_SECRET`.** | 1 | 2 | 1 | 2 | 1 | code health by 2 | 14.4 |
| 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 |
| 14 | 0.5.0 | | | **Drop a double-clicked duplicate submit, with no client-side JavaScript.** | 4 | 2 | 3 | 7 | 7 | 2 by 7, 1 by 4 | 1.4 |
| 19 | 0.6.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.6.0 | | question | **Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.** | 3 | 2 | 3 | 6 | 6 | 7 by 2 | 0.5 |
| 28 | 0.6.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.6.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.6.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.6.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.6.0 | | | **List grants that no installed plugin declares, with a remove action.** | 2 | 2 | 2 | 4 | 4 | 2 by 3 | 2.1 |
| 41 | 0.6.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 |
| 14 | 0.6.0 | | | **Drop a double-clicked duplicate submit, with no client-side JavaScript.** | 4 | 2 | 3 | 7 | 7 | 2 by 7, 1 by 4 | 1.4 |
## Renamed
- 0.4.21 renamed to 0.5.0
- 0.4.22 renamed to 0.5.0
## Details
### 56. Correct the README's two statements that `REQUIRE_SECURE_SECRETS` guards only `CSRF_SECRET`.
README → What you must supply (about `:1275`: "refuses to boot on a missing or throwaway
`CSRF_SECRET` and **nothing else**") and README → Security model (about `:1442`:
"`REQUIRE_SECURE_SECRETS` guards only `CSRF_SECRET`") contradict the Configuration table row (about
`:1216`), `src/config.ts:59` and `:190`, and `src/plugin-host/settings.ts`. The flag also refuses a
missing or throwaway `PLUGIN_DB_SECRET` once `PLUGIN_DB_URL` is set, and every plugin setting
declared `secret`. An operator following the checklist underrates the toggle. Make both passages say
what the flag enforces by pointing to the table row, its one home. Four comprehension seats raised it
independently. Markdown only.
### 19. Run the documented Docker commands on macOS and fix whatever misbehaves.
Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id -g` is `20`, which is
`dialout` inside the noble image, and Docker Desktop remaps bind-mount ownership in its VM.
### 16. Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.
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`
@@ -74,17 +47,6 @@ into named steps, say in the header that an unsupported value in a custom proper
`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
@@ -104,11 +66,15 @@ 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.
### 19. Run the documented Docker commands on macOS and fix whatever misbehaves.
`/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.
Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id -g` is `20`, which is
`dialout` inside the noble image, and Docker Desktop remaps bind-mount ownership in its VM.
### 16. Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.
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.
### 28. Let a plugin add entries to the profile menu through the plugin API.