diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ab04f1..bdf59ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,9 @@ at boot. Entries start at 0.3.0. ### Discovery refuses a plugin with the id `error` -`/error` is the page Kratos sends a failed sign-in 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. +`/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 diff --git a/README.md b/README.md index 2a1cffc..3738399 100644 --- a/README.md +++ b/README.md @@ -417,7 +417,7 @@ URL/path-safe (`isValidPluginId`: lowercase `a–z`, digits, dashes) and also na `views/`, its `/public//` 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. Of these, `REQUIRE_SECURE_SECRETS=true` checks only - `CSRF_SECRET` (its [Configuration](#configuration) row lists the other secrets it checks); 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 | | --- | --- | --- | diff --git a/src/auth/bootstrap.ts b/src/auth/bootstrap.ts index ab08cae..5aeed8a 100644 --- a/src/auth/bootstrap.ts +++ b/src/auth/bootstrap.ts @@ -28,8 +28,6 @@ export function identityPayload(email: string, password: string) { }; } -// Coarse-permission grant: `Permission:#granted@user:`. Subject ids are `user:` -// (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}` }; } diff --git a/src/auth/login.ts b/src/auth/login.ts index 65948ec..d24849b 100644 --- a/src/auth/login.ts +++ b/src/auth/login.ts @@ -39,7 +39,7 @@ export interface CompletedLogin { // The coarse permissions a user holds — directly (`Permission:#granted@user:`) or transitively via a // group granted 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 — +// 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 { diff --git a/src/auth/routes.ts b/src/auth/routes.ts index a942537..e27a02c 100644 --- a/src/auth/routes.ts +++ b/src/auth/routes.ts @@ -117,8 +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. `submitFlow` throws only on a 5xx or an unreachable Kratos, so of -// `flowFailure`'s branches only the 503 fires 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 => { const pathname = ctx.url.pathname; diff --git a/src/plugin-host/plugin.test.ts b/src/plugin-host/plugin.test.ts index 34e6e86..4f0d976 100644 --- a/src/plugin-host/plugin.test.ts +++ b/src/plugin-host/plugin.test.ts @@ -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. Only the auth flows derive from -// AUTH_FLOWS; every other mount is listed by hand, so a new top-level route needs a line here too. +// 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([