chore: upgrade FastAPI/Starlette, add security response headers - #6
Merged
Merged
Conversation
FastAPI 0.128.8 -> 0.141.1 and Starlette 0.52.1 -> 1.6.0, which is what the six open Dependabot alerts needed; also pytest 9.1.1, ruff 0.16.3 and uvicorn 0.52.3. Alerts are evaluated against the default branch, so they clear at the first promotion to main, not on this merge. Bumping fastapi alone did not move starlette: uv reported success and left the pinned transitive version in place, so it needed an explicit --upgrade-package. Worth knowing before trusting a lockfile diff. The jump broke the route-enumeration security test silently. Since 0.137 include_router stores a tree node instead of copying routes onto the parent, so walking app.routes returned three routes where it returned nine — and still passed, because a walk over nothing finds no unprotected endpoints. The walk now goes through iter_route_contexts, and a canary asserts a known-protected route is visible in it and absent from PUBLIC_ROUTES, so this file cannot go vacuous unnoticed again. Headers arrive in two layers. vercel.json covers what the CDN serves; a plain ASGI middleware covers /api/* in process, so the set also holds under bare uvicorn and can be asserted in CI. It wraps send rather than subclassing BaseHTTPMiddleware, which is what puts headers on the responses no endpoint produced: 401, 403, 404, 422. The edge rule deliberately overlaps /api/*, because a 500 from an unhandled exception comes from ServerErrorMiddleware, built outside the user middleware stack, and never reaches the wrapper. The document policy carries no unsafe-* token: React sets inline styles through CSSOM, which CSP does not govern, so style-src stays 'self'. Strict-Transport-Security is left to Vercel, which already sends it — a duplicate field is ignored rather than merged. Cross-Origin-Resource- Policy is never set, since same-origin would stop the shell loading remoteEntry.js in PR #5. Permissions-Policy lists only what it restricts, leaving screen-wake-lock, fullscreen and autoplay at their defaults for the session player. The /api/docs CSP exemption is derived from the configured docs URLs rather than written as a literal, so it is empty in production where they are None. Adds .github/dependabot.yml for uv, npm and github-actions, grouped and weekly, with @types/node held at the Node 24 major. Dependabot reads the config from the default branch only, so a byte-identical twin on main is required before it does anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced Aug 14, 2026
kilianmc
added a commit
that referenced
this pull request
Aug 14, 2026
#14) * feat: router + query + dual entry (browser/memory history) + MF expose Frontend infrastructure for every screen that follows. One route tree, two histories, and the remote contract the shell will consume in PR #5. Every route body is a placeholder; what this PR establishes is the wiring. - web/src/router.tsx — createAppRouter(history) + createQueryClient(). The history is the ONLY difference between the two mounts, so every other default lives here and cannot drift. defaultPreload: 'intent', defaultPreloadStaleTime: 0 (Query owns staleness; a second cache with its own expiry would disagree with it), pendingMs 300 / pendingMinMs 500. - web/src/main.tsx — standalone: browser history, StrictMode, and createRoot's onCaughtError/onUncaughtError so a failed mount is not a blank page and a silent console. Still the only file that may ever register a service worker. - web/src/remote.tsx — federated: createMemoryHistory, default-exported component, router and QueryClient per mount instance rather than per module so state does not survive shell navigations. No StrictMode (the host owns that). - web/src/routes/ — __root (the one .ct-app element), index, login, four lazy leaves, and a catch-all. routeTree.gen.ts is committed deliberately. - vite.config.ts — @tanstack/router-plugin + @module-federation/vite exposing './App' as climbTrainer, react/react-dom singletons at ^19.0.0 plus the scoped 'react/' and 'react-dom/' shares, build.target chrome89 for top-level await. - vercel.json — Access-Control-Allow-Origin: * on /remoteEntry.js and /assets/* only. The MF chunks load as modules, which are always CORS-mode; /api/* keeps its allowlist. - styles split by mount: app.scss from __root.tsx (both mounts, zero :root rules), global.scss from main.tsx (the document reset). This is what makes the .ct-app rule mechanical instead of a convention. Two things learned that would otherwise be re-derived, both in CLAUDE.md: createLazyFileRoute inside a plain plan.tsx builds, warns nothing and bundles EAGERLY. Only the <route>.lazy.tsx filename makes the generator emit a dynamic import, so renaming one of those files silently deletes its code-splitting. routeTree.gen.ts must stay committed: vitest.config.ts replaces vite.config.ts rather than merging it, so the router plugin never runs under Vitest and cannot regenerate the tree. Verified by deleting it and watching vitest fail. Tests cover the router factory under memory history (render, a lazy hop, the catch-all) and guard the three federated-mount rules at runtime — no service worker, no history mutation, no un-namespaced localStorage. The placeholder bodies are untested on purpose, per the testing policy. No query-cache persistence: it needs the demo-scope exclusion, which needs auth state that does not exist until PR #6. It lands in PR #14. Opens 1.6.0. Co-authored-by: Kilian Mateo <13885240+kilianmc@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: harden the remote guard, which passed while all three rules were violated remote.guard.test.tsx went green against the most likely PR #7 regression. Three jsdom facts caused it, each confirmed by probe: - document.readyState is already 'complete' when a test runs, so a listener added during render never fires. virtual:pwa-register (vite-plugin-pwa) registers from exactly such a window 'load' listener. Now dispatched. - localStorage.foo = 'x' writes the value while bypassing Storage.prototype.setItem, so the spy missed it. Now asserts on Object.keys(localStorage). - clear() and removeItem() were not spied at all, and leave no trace in the final state. A remote calling clear() wipes the portfolio's storage. Module-scope side effects also ran before any spy existed, so the entry is imported dynamically with resetModules per test. Adds a positive control asserting each detector can see its own violation, since every storage assertion otherwise passes on an empty set. Converts two documented "don't touch this" rules into assertions: - mf-contract.test.ts: vercel.json must keep ACAO on /remoteEntry.js and /assets/*, and nowhere else. Both are load-bearing off-repo — remoteEntry.js statically imports /assets/virtual_mf-REMOTE_ENTRY_ID*.js, and the exposed chunk preloads its own CSS — so deleting either kills the federated mount with a fully green gate. - routeTree.lazy.test.ts: the four heavy leaves must be code-split. Renaming plan.lazy.tsx to plan.tsx passes lint, typecheck, test AND build while emitting no separate chunk. Two comments corrected rather than left to become ground truth: - build.target 'chrome89' removed. Vite 8's default is already chrome111+ and keeps MF's top-level await; the pin only lowered the baseline (it also forced lightningcss color-scheme fallbacks). - the nav's "44px touch targets" claim was false on three of six links (31.1/37.7/43.8px wide). min-inline-size added; measured 44x44 in Chrome at 320px and 390px, row gap 4px -> 8px. --------- Co-authored-by: Kilian Mateo <13885240+kilianmc@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kilianmc
added a commit
that referenced
this pull request
Aug 17, 2026
Addresses the independent review on #20. The first pass only fixed `errorComponent`. But a root-level not-found, a pending root match, and the router's two root-level Suspense fallbacks (Matches and MatchView both build theirs from the ROOT route's pendingComponent) all replace RootLayout the same way, taking the `.ct-app` element with them — so they would have rendered unstyled into kilianmc.com's document too. Unreachable today, since the root has no loader or beforeLoad, but PR #6's auth bootstrap is exactly what makes it live. The wrapper therefore moves into ui/status.tsx and covers all three renders. It is conditional: `.ct-app` carries padding, a max inline size and a background, so wrapping unconditionally would inset the layout twice for a leaf error inside the outlet. RootLayout marks its outlet with CtAppScope and the status renders skip their own wrapper inside it. RootError is gone — one mechanism, three slots. Also, `git diff --exit-code` exits 0 on a pathspec that matches nothing, so the route-tree check could have rotted invisibly on a rename; the file's existence is now asserted first. The check stays CI-only on purpose: locally the diff is worktree-vs-index, so any branch that legitimately adds a route file would be red until the regenerated tree is staged, and a gate that is red by default gets ignored. CLAUDE.md's local-vs-CI check counts are corrected instead, with that residue written down. CLAUDE.md: the bridge-failure log is four lines, one per shared key, against a production build; the dev-server arm splits differently, which is why the rule is to grep the string rather than assert a count. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 18, 2026
kilianmc
added a commit
that referenced
this pull request
Aug 20, 2026
Cross-file deduplication and size reduction across CLAUDE.md, the README and the private notes. NOT the prose-verbosity trim, which stays deferred until the app is finished. Rule applied throughout: trim status, keep rationale. - CLAUDE.md gains a heading-anchored index grouped by task, so a coder starting a PR can find one rule without reading 2000 lines. Anchored to heading text, never line numbers, which rot. - Eight contradictions resolved against the code rather than guessed. The significant one was public: CLAUDE.md and the README both claimed V-scale, Font and French grades are directly comparable. server/domain/grades.py puts boulder in the 1000-band and rope in the 2000-band and raises CrossDisciplineError, so V5 and 7A compare but 6c+ does not. - README no longer calls dev the default branch (main is), and points at CLAUDE.md instead of restating the testing policy, CI internals and auth internals. Headings de-versioned so anchors stop rotting. - The byte-identical-twin rule's add/add half is marked expired; the "inert until the next promotion" half stands. - REPLAY_GRACE keeps the operative one-line constraint here; the analysis moved to private notes. - The query-cache persistence ban keeps its rule and loses its expired reason. The hazard is that localStorage belongs to the shell, not PR #6 sequencing. It does not constrain the outbox. - Adds two rules that were missing: migrate production BEFORE promoting, and hand the migrate.yml dispatch to Kilian rather than attempting it. - migrate.yml is comments only: drops the revision numbers that went stale at 0003, keeps the ref-vs-environment distinction. No version bump, matching #32. Co-authored-by: Kilian Mateo <13885240+kilianmc@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Raises the six Dependabot-alerted versions and adds the security response header baseline (item 9 of the end-to-end security verification pass). Version
1.3.0.Dependencies
Every other pin was checked against PyPI and is already at the latest published version.
The alerts will not clear on this merge. Dependabot evaluates the default branch, which is
main, andmainstill carriesstarlette 0.52.1. They clear at the first promotion.Two things the upgrade found
uv lockdid not move starlette. Bumpingfastapireported success and left the transitive pin in place — the alerts were againststarlette. It needed--upgrade-package starlette. A PR claiming to close these alerts could have closed none of them, with a green gate.include_routernow stores a tree node rather than copying routes onto the parent, so walkingapp.routesreturned 3 routes instead of 9 — and still passed, because a walk over nothing finds no unprotected endpoints. Fixed withiter_route_contexts, plus a canary asserting a known-protected route is visible in the walk and absent fromPUBLIC_ROUTES, so it cannot be neutered by making that route public.Also noted for PR #6: FastAPI 0.132 enables
strict_content_typeby default, so a POST withoutcontent-type: application/jsonnow 422s —apiFetchcurrently sends onlyaccept.Headers
Two layers on purpose:
vercel.jsonfor what the CDN serves, and a plain ASGI middleware for/api/*in process, so the set also holds under bareuvicornand is assertable in CI. The middleware wrapssendrather than subclassingBaseHTTPMiddleware— that is what puts headers on responses no endpoint produced (401/403/404/422), which the tests cover.Deliberate choices, each with a test or a comment:
unsafe-*token in the document CSP. React sets inline styles through CSSOM, which CSP does not govern, sostyle-srcstays'self'.max-age=63072000; a duplicate field is ignored rather than merged.Cross-Origin-Resource-Policynever set —same-originwould block the shell loadingremoteEntry.jsin PR chore: stop the migrate job printing the Neon endpoint (main-side twin) #5. This, notframe-ancestors, is the header that would break the federated mount; CLAUDE.md's item 9 said otherwise and is corrected. The MF mount loads this app as a script, not an iframe.Permissions-Policylists only what it restricts, leavingscreen-wake-lock,fullscreenandautoplayat their defaults for the session player. Asserted by test./api/docsCSP exemption is derived from the configured docs URLs, not a literal, so the exempt set is empty in production. Asserted by test./(.*)edge rule intentionally overlaps/api/*: a 500 from an unhandled exception comes fromServerErrorMiddleware, built outside the user middleware stack, so the in-process layer cannot reach it. Whether Vercel overwrites or appends on function responses is unverified — item 9 mustcurl -sIan/api/*path on the real deploy and confirm no header appears twice.Dependabot
.github/dependabot.ymlforuv,npm(/web) andgithub-actions, weekly and grouped, with@types/nodeheld at the Node 24 major.Two traps, both verified against GitHub's docs. The config is read from the default branch only, so a byte-identical twin on
mainis required or this file does nothing — same failure mode asmigrate.yml. Andtarget-branch: dev(needed, sincemaintakes only promotion PRs) means security updates would not use this config; that is currently moot becauseautomated-security-fixesis disabled on the repo. The file carries no comments so the two copies cannot drift.Also corrected: CLAUDE.md described
devas the default branch. It ismain.Gate
ruffclean with no ignores widened,ruff formatclean,mypy33 files, 80 tests passed (was 70), build OK. One warning:starlette.testclientnow prefershttpx2overhttpx— a package swap, not a version bump, deferred as its own change before Starlette 2.0.🤖 Generated with Claude Code