§10 public pages + menu items, the blessed explicit alias (todo §10); a plugin may mark a page and its menu option public. A no-permission route/nav node is already anonymous-reachable, so per the human's pick this BLESSES that as a first-class, explicit choice (keep the default; add an explicit alias — not a secure-by-default flip). New optional public?: boolean on Route (src/plugin.ts) + NavNode (src/nav.ts) = "open to everyone, signed in or not", honored outright in isAuthorized (router.ts) + filterByRoles (nav.ts), and MUTUALLY EXCLUSIVE with permission — discovery shapeError recursively rejects a route/nav node setting both, failing the boot loud (never silently picks one). public is filter-only (toRenderNode never emits it). The shell (views/partials/shell.ejs) now renders a Sign in link instead of the profile/sign-out block for an anonymous visitor, so a public page in the native shell (ctx.chrome; ctx.user may be null) isn't a broken "Guest / Sign out". Reference plugin demos it: a public /scheduling Overview route + a public "Overview" nav child (the "Scheduling" header now shows for everyone), the shifts list still behind scheduling:read. Hardened the latent gap the shell newly leans on: claimsToUser rejects an empty email like it does an empty sub. Tests-first (348 → 354 units): router/nav/discovery (public open + reject-both + loads), shell (anon → Sign in, no logout form), app (public route anon-200), shifts (overview handler), jwt-middleware (empty email). Docs: plugin-contract.md ("Public pages & menu items" + route shape + shape-error note) + README (menu system + reference snippet). E2E: visual.spec asserts the public Overview is anon-200 + shown in the member's nav while the gated Shifts redirects/filters. stability-reviewer: APPROVE, no Critical/High/Medium (addressed its one Low — the empty-email hardening). typecheck + 354 units + full scripts/ci.sh gate (visual 10 · auth 1 · oauth 2 · full 7) green.

This commit is contained in:
2026-06-20 18:12:46 +02:00
parent 7787ed4ea4
commit 7bdeb24b7f
20 changed files with 210 additions and 45 deletions

View File

@@ -45,13 +45,14 @@
<%- include("theme-switch", { value: locals.theme }) %>
<div class="footer-actions">
<%# profile menu stays inline: the summary composes escaped user values %>
<% if (user.email) { %>
<%# signed in: profile menu inline (the summary composes escaped user values) %>
<details class="menu" style="flex:1 1 auto">
<summary class="profile">
<span class="avatar" aria-hidden="true"><%= user.initials %></span>
<span class="profile-meta">
<span class="profile-name"><%= user.name %></span>
<% if (user.email) { %><span class="profile-mail"><%= user.email %></span><% } %>
<span class="profile-mail"><%= user.email %></span>
</span>
</summary>
<div class="menu-pop left up" style="min-width:220px">
@@ -64,6 +65,10 @@
</form>
</div>
</details>
<% } else { %>
<%# anonymous (a public page in the shell, §10): no session to end — offer a way in instead %>
<a class="btn btn-primary" href="/login" style="flex:1 1 auto"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-user" /></svg>Sign in</a>
<% } %>
<%- include("menu", {
up: true,