168 records -> docs/decisions/records/<area>/<topic>.md (163 active, 23 dirs) and docs/decisions/archive/<area>/<topic>.md (5 archived). The filename IS the key, so one-active-record-per-key becomes a filesystem property rather than a validator check, and supersession becomes a `git mv`. WHY: the monolith was a concurrency problem before an aesthetic one. A 3,900-line append target made parallel sessions collide -- PR #605 and PR #614 both hit append-vs-append conflicts during routine rebases, and hand-resolving those inside the corpus is exactly the operation the rationale-rewrite guard exists to police. HOW IT IS VERIFIED: a ~170-file diff cannot be meaningfully read, so correctness does not rest on reading it. The parser was taught BOTH formats first, so the body-diff guard parses the old form at the merge-base and the new form at head -- the migration validates itself, no bypass. The proof is a field-level equivalence harness: 168 records before and after, zero lost, zero gained, zero field mismatches, zero rationale bodies differing. Reviewers should scrutinise the harness; it is the actual evidence. What measuring caught that reading would not have: - ~500 lines sit OUTSIDE any record -- decisions.md's lifecycle schema and each topic file's preamble, mostly the only copy. Source files are kept and stripped, never deleted. They also cannot be filed per-area: topic files hold several areas and 4 of 23 areas span several files. - Archive discovery was a non-recursive glob; after the split it found ZERO archived records, surfacing as four bogus "supersedes points to unknown key" errors rather than an obvious failure. - ~32 live docs point into the corpus BY DATE, which the split dangles. Each stripped file now ends with a generated "Records formerly in this file" index, which also rescues the identical breadcrumbs in old issue comments. - decisions.md's "In this file:" list was 97 same-file anchor bullets that the split makes WRONG, not merely stale. Dropped; the generated index replaces them with links that resolve. The equivalence harness now runs against a checked-in FIXTURE, not the live corpus. The earlier version migrated the real tree, which made it a one-shot: the moment the migration landed there was nothing left to move and the tests failed for reasons unrelated to the code. A fixture keeps them testing the SCRIPT rather than the repo's current state. Keys preserved verbatim, warts included: `sched` (12) and `scheduling` (1) remain two directories for one concept. Renaming a key is not a move -- it changes identity, breaks the equivalence proof, and invalidates MemPalace's per-key drawers. Taxonomy normalisation is separate work. refs #610
4.1 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| security.blazor-removal-auth-posture | 2026-07-11 — Blazor removal auth posture: no new exposure beyond phase (a); real auth deferred to #197 (#206) | active | 2026-07-11 | none | none | Removing the Blazor UI's OIDC-challenged surface exposes nothing a user couldn't already reach via the already-open `/app` SPA (open since phase (a)); real SPA/API authentication is deliberately deferred to #197, and the removal PR must preserve `ConditionalIptvAuthorizeFilter`, `ApiKeyAuthorizationFilter`, and `JwtHelper` access_token support. | Blazor removal, auth posture sign-off, OIDC attachment point · paths: `ErsatzTV/Startup.cs`, `ErsatzTV/Pages` · issues: #206, #91, #197 | `ErsatzTV/Startup.cs` (Razor Pages/OIDC registration); #91 phase (b) removal PR |
Sign-off for the #91 phase (b) removal-gate item #206 ("deleting the last challenged Blazor page leaves
only the open SPA"). The actual authorization wiring in ErsatzTV/Startup.cs + ErsatzTV/Pages was
enumerated in code (not assumed) before clearing the gate.
What is gated today
- OIDC (
OidcHelper.IsEnabled— active only whenAuthority/ClientId/ClientSecretare configured):AddAuthentication(cookie default,oidcchallenge) +AddAuthorizationDefaultPolicy = RequireAuthenticatedUser+AddRazorPages(… AuthorizeFolder("/"))(Startup.cs:379-385) +blazor.UseAuthentication()/UseAuthorization()inside the BlazorMapWhenbranch (Startup.cs:764-770).AuthorizeFolder("/")gates Razor Pages only, and the sole user-facing Razor Page isPages/_Host.cshtml— the Blazor Server host (the other.cshtml,Shared/_Favicons.cshtml, is a cosmetic partial). So the OIDC challenge protects exactly the Blazor UI and nothing else. /app(SPA) is served by its ownMapWhen(path=/app)static-file branch (Startup.cs:701-714) with no authentication/authorization middleware — open since phase (a) (/→/app, PR #148)./api/*controllers carry no[Authorize](verified: zero attributes inControllers/); the Razor-PagesAuthorizeFolder/DefaultPolicynever reach them. Their only optional gate is the per-endpointApiKeyAuthorizationFilter(API-key on mutating JSON endpoints), independent of OIDC/Blazor./iptv/*is gated byConditionalIptvAuthorizeFilter(JWTJwtOnlyScheme, active only whenJwtHelper.IsEnabled) in its ownMapWhenbranch (Startup.cs:797-803) — independent of Blazor.
Posture after Blazor removal. Removing Pages/_Host.cshtml, AddRazorPages/AuthorizeFolder("/"),
blazor.UseAuthentication/UseAuthorization, MapBlazorHub, and MapFallbackToPage("/_Host") deletes the
OIDC challenge's only attachment point — no user-facing surface remains challenged. No capability is
lost: every Blazor-served capability already has an open SPA equivalent (the #91 parity effort), and the
SPA was already the unauthenticated path since phase (a), so removal exposes nothing a user could not already
reach via /app.
The one honest caveat (not a regression introduced by removal): an OIDC-configured operator's Blazor admin UI sits behind a login today; after removal there is no login-gated admin UI at all (the SPA admin UI is open). That exposure delta already happened at phase (a) (the open SPA became the default admin surface); removal only deletes the now-redundant challenged duplicate. Designing real SPA/API authentication is deliberately deferred to #197 (cold API security review — a HARD GATE before any remote exposure).
Removal-PR must-not-break (independent gates that survive): ConditionalIptvAuthorizeFilter (/iptv/*
JWT), ApiKeyAuthorizationFilter (mutating /api/*), and JwtHelper access_token query support. Leave
the OIDC service registrations in place (conditional on config, inert once no Razor Page consumes them) —
ripping OIDC out is a #197 decision, not a removal-PR one. The removal PR removes only the Blazor-attached
pieces above; MapControllers() + /docs (Scalar), currently co-hosted in the Blazor MapWhen branch, must
survive the surgical reduction.