Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.1 KiB
RB-15 — Swagger and the OpenAPI document behind IsDevelopment()
Status: implemented · 2026-08-27 · Source findings: 07-bio2-compliance.md BIO-015 · 99-backlog.md RB-15
What was wrong
Program.cs:145-146 (pre-change) ran app.UseSwagger(); app.UseSwaggerUI();
unconditionally — the full OpenAPI document (every route, every request/response shape)
and SwaggerUI's interactive "Try it out" were reachable in every environment, including a
real deployment, with no app.Environment.IsDevelopment() guard. BIO-015's own evidence
notes this is one line of genuine attack-surface reduction with no POC cost.
What changed
| File | Change |
|---|---|
Program.cs |
app.UseSwagger(); app.UseSwaggerUI(); now run only inside if (app.Environment.IsDevelopment()) { … } |
tests/BigRegister.Tests/SwaggerGateTests.cs |
new — asserts /swagger/v1/swagger.json is served in Development and 404s outside it |
builder.Services.AddSwaggerGen(...) and AddEndpointsApiExplorer() were left
unconditional — they only register DI services (the swagger-generation machinery),
expose nothing over HTTP by themselves, and (see below) are exactly what npm run gen:api depends on staying registered in every environment it might run against.
The hazard, checked rather than assumed
RB-09 made a non-Development environment throw during builder.Build() (no
IIdentityProvider registered for a bare/unset environment, which defaults to
Production), which crashed dotnet swagger tofile until package.json's gen:api
script was pinned to ASPNETCORE_ENVIRONMENT=Development for that one invocation
(docs/.../implementation/rb-09.md). This ticket's change sits in exactly the same
pipeline, so it needed the same empirical check, not an assumption.
Mechanism, confirmed by reading Swashbuckle's CLI behaviour and then proving it:
dotnet swagger tofile (Swashbuckle.AspNetCore.Cli) loads the built DLL through
.NET's design-time HostFactoryResolver, builds the host, and then resolves
ISwaggerProvider directly out of the DI container to produce swagger.json — it
never issues an HTTP request through the ASP.NET Core middleware pipeline this ticket's
if (app.Environment.IsDevelopment()) guard lives in. Gating UseSwagger()/
UseSwaggerUI() therefore cannot affect it, in any environment, by construction — those
are pipeline middleware; the CLI tool bypasses the pipeline entirely.
Verified, not assumed: ran npm run gen:api for real. It exited 0, printed "Swagger
JSON/YAML successfully written to …/backend/swagger.json", and regenerated the NSwag
client. git status/git diff on both backend/swagger.json and
libs/shared/src/infrastructure/api-client.ts showed zero changes — the regenerated
files are byte-identical to what's already committed, confirming the gate has no effect
on the generated contract at all.
Judgement calls
- The guard wraps both
UseSwagger()andUseSwaggerUI()together, not just one — the ticket's own wording lists both, and gating only the document while leaving the UI reachable (or vice versa) would be a strange half-measure: SwaggerUI without the document 404s on load anyway, and the document without the UI still leaks the same route/shape enumeration BIO-015 is about. AddSwaggerGen/AddEndpointsApiExplorerwere left unconditional. They're DI-registration-time calls with no HTTP surface, and — now confirmed rather than assumed —dotnet swagger tofileneedsISwaggerProviderregistered in whatever environment it runs the host under (pinned to Development bygen:api's own script, but nothing stops a future non-Development invocation), so conditioning those registrations onIsDevelopment()would risk breaking the CLI tool for no attack-surface benefit — nobody can reach a DI-registered-but-never-routed service over HTTP.- New tests build the "non-Development" case on a third environment name
("Staging"), not
"Production". RB-09 already made Production fail at startup entirely (no realIIdentityProviderexists yet) — a stronger guarantee than "no Swagger in Production," but one that means aUseEnvironment("Production")host never reaches this middleware to prove the gate itself works; it only proves RB-09's unrelated startup throw, which already has its own test. A"Staging"environment satisfies neitherIsDevelopment()norIsProduction(), soProgram.csregisters noIIdentityProviderfor it — the test supplies one viaConfigureTestServices(StubIdentityProvider, the same one Development uses) so the host actually boots, and the test exercises this ticket's real gate rather than a different ticket's. - The Staging host is built via
factory.WithWebHostBuilder(...)(layering on the sharedTestWebApplicationFactoryfixture), not a barenew WebApplicationFactory<Program>()— RB-12's implementation note already records the "table already exists" collision a bare factory hits by sharing the mutable staticDb.ConnectionStringinstead of the fixture's own per-class isolated temp path; layering avoids repeating that mistake here.
Verification
- Reverted the guard only (unwrapped
UseSwagger()/UseSwaggerUI()back to unconditional, via Edit, tests left in place) and ranSwaggerGateTests:Swagger_document_is_not_served_outside_developmentfailed red (Expected: NotFound, Actual: OK). Restored the fix (via Edit) and re-ran: both green. npm run gen:api, run for real: exit 0;backend/swagger.jsonandlibs/shared/src/infrastructure/api-client.tsboth unchanged (git statusclean on both) — see "The hazard" above.dotnet build(both projects): clean, 0 warnings.dotnet format BigRegister.slnx --verify-no-changes: clean.dotnet test --filter "Category!=Integration": 259 passed, 0 failed (257 + 2 new).