Added
auto_navigate— opt-in automatic SPA link interception (#1734, ADR-021 Stage 2). Withdj-navigateyou annotate each link;auto_navigategoes one step further — a single delegated click listener SPA-navigates plain<a href>links (no djust attribute needed) whenever the link's path resolves in the route map. Enable viaLIVEVIEW_CONFIG['auto_navigate'] = True(default OFF);{% djust_client_config %}then emits a<meta name="djust-auto-navigate">flag (CSP-clean, no inline script) and the client installs one delegateddocumentlistener. It is deliberately conservative — a link falls through to a normal browser navigation on a modifier/middle click, atargetother than_self, adownloadattr,rel="external", adata-no-navigateancestor, an external origin or non-http(s) scheme, a same-page hash-only jump, or any path not in the (auth-filtered, #1758) route map — so admin pages, plain Django views, and routes the user can't access reload normally and the server enforces access. Same-view query-only changes uselive_patch(state-preserving); cross-view useslive_redirect. Nativedj-navigate/auto_navigateis positioned as djust's canonical SPA model;turbonav-integration.mdis reframed as interop (#1735). NewTestRouteMapAuthFilter-adjacent JS suite (tests/js/auto_navigate_1734.test.js, 15 cases incl. the full opt-out matrix) +TestClientConfigemit tests.djust deploypreflight "deploy doctor" warns on settings that violate the platform env contract (#1760). A foreign (non-scaffold) app — one written for on-device/loopback use — could deploy "successfully" and then 500 silently in production because its Django settings ignore the env values the platform injects.djust deploy/djust deploy-dirnow run a non-blocking static preflight (_run_deploy_doctor) over the resolved settings module (located viamanage.py'sDJANGO_SETTINGS_MODULE, falling back to the shallowestsettings.py) and print warnings to stderr — never errors, the deploy still proceeds — when it finds: a hardcoded literalSECRET_KEY(dev key on a public host), anALLOWED_HOSTSliteral not read from env (→DisallowedHost400), aDATABASESblock that never consultsDATABASE_URL(→OperationalError: readonly databaseon the read-only app rootfs), or a sqliteENGINE(sqlite under the read-only project dir 500s on first write). Each warning ends with the env-injection pointer. The checks are grep-level static inspection of the settings source text — the module is never imported, so an unimportable foreign settings file can't break the doctor, and the whole pass is wrapped fail-soft so a doctor error can never block a deploy. Prevents the silent "successful deploy, 500s at runtime" chain that cost a multi-hour production debug. 14 new cases inTestDeployDoctor.djust deployshows "rolling out" instead of "active" while a blue/green rollout is still serving stale code (#1761). djustlive'sdeployment_statusendpoint now returns an additiveserving_currentboolean (djustlive #517) —Falsewhile a new rootfs is built and marked current but the old placement is still serving the env URL during cutover. Thedeploy-dirpoll loop now consumes it: while the deployment row readsactive/deployingbutserving_currentisFalse, the CLI printsStatus: rolling out (new version built; old version still serving — waiting for cutover)and keeps polling, instead of reportingactiveand printing the URL on stale code (which led users to re-test against the old rootfs). A missing field is treated asTrue(fail-safe), so the behavior is byte-identical against older servers that don't send it. The poll decision is extracted into a pure_poll_display(data) -> (message, done, url)helper. 8 new cases inTestPollDisplay.
Security
Documented
LIVEVIEW_ALLOWED_MODULESas recommended production hardening (#1778, threat model T4). The WebSocket mount allowlist is enforced only when non-empty; an unset list lets a client request mounting anyLiveViewby path (enumeration — not an auth bypass, since per-view auth still gates). Added guidance todocs/guides/security.md+docs/SECURITY_GUIDELINES.mdto set it to your app's module prefixes in prod (djust.V005flags views outside a non-empty list but cannot warn about an unset one).Opt-in per-event auth re-check on the WebSocket path (#1777, threat model T3, defense-in-depth). Auth runs at mount; the connect-time scope user is cached, so an authenticated user who logs out or loses a permission mid-session keeps dispatching events on the open socket until they reconnect. New
LIVEVIEW_CONFIG['reauth_on_event'](default OFF): when enabled and the mounted view declareslogin_required/permission_required,handle_eventre-resolves the user from the session (channels.auth.get_user) and re-runs the view's auth check, sending a navigate redirect +close(4403)+ clearingview_instanceon failure. Default OFF because it costs one session read per event — opt in for high-security apps that want mid-session deauthorization enforced on the live path. Fail-safe (skips the check, never breaks the event) when there is no session in scope. Behavior note: when the flag is on, the re-resolved (current) user is also written torequest.user, so event handlers observe live auth state rather than the connect-time snapshot. Complements the T1/T2 mount-redirect fix.WebSocket auth bypass fixed: a
login_requiredmount redirect now closes the socket instead of leaving it open (threat model T1/T2).handle_mountsent a{"type":"navigate"}redirect frame on thelogin_required(andon_mount-hook) failure branches but did not close the socket — only thePermissionDeniedbranch closed it (4403). The view never mounted yetview_instancestayed set, andhandle_eventnever re-checks auth, so a raw WebSocket client that ignored the navigate frame could send{"type":"event"}messages and reach@event_handlermethods with no authenticated session — a full auth bypass on the live mutation path (a browser obeys the redirect and hides it). Reachable via both the initial mount andhandle_live_redirect_mount(which delegates tohandle_mount). Both redirect branches now send the navigate frame, thenclose(code=4403)and clearview_instance, mirroring thePermissionDeniedbranch; public/authorized mounts are unchanged. AWebsocketCommunicatorreproducer (anonymous scope) proves the bypass pre-fix and the close post-fix. The complete WS auth/transport threat model — 9 threats, including T3 (the event path does not re-check auth mid-session) and T4 (LIVEVIEW_ALLOWED_MODULESis default-open), tracked as follow-ups — is documented indocs/audits/websocket-auth-2026-06.md.The auto-emitted client route map is now auth-filtered — gated routes no longer leak to clients that can't access them (#1758, ADR-021 Stage 2).
window.djust._routeMapwas built bybuild_route_map_from_urlconfwith no auth filtering and emitted to every client (including anonymous visitors), enumerating all LiveView routes —login_required/permission_required/ admin ones included — each with its dottedmodule.QualNameview-class path. An anonymous visitor to a public page therefore learned the full route table plus the internal view-class names of routes they cannot reach (recon-grade information disclosure; not an auth bypass — the WS mount path allowlists modules and views still enforce auth at mount).get_route_map_script(request)— the single funnel both template engines use — now omits any route whoseLiveViewdeclareslogin_required/permission_required(or whose callback islogin_required(as_view())-wrapped, or which uses Django'sLoginRequiredMixin/PermissionRequiredMixin) unlessrequest.usersatisfies it, and fails closed for anonymous / no-request callers. Public routes are unaffected, so existing public apps emit an identical map. A gating sidecar is built by the same single URLconf walk (no extra cost). NewTestRouteMapAuthFiltercases + a dedicated fixture URLconf.Bump PyO3 0.25 → 0.29 to fix two advisories — GHSA-36hh-v3qg-5jq4 (High) and GHSA-chgr-c6px-7xpp (Moderate) (#103, #104). Both advisories cover all PyO3 versions
< 0.29.0with no backport: an out-of-bounds read innth/nth_backforPyList/PyTupleiterators (High), and a missingSyncbound onPyCFunction::new_closureclosures (Moderate). No Dependabot PR existed for either. Bumpspyo3andpyo3-async-runtimes0.25 → 0.29 and migrates the FFI layer across the 0.26–0.29 breaking changes:Python::with_gil→Python::attachandPython::allow_threads→Python::detach(GIL attach/detach rename), the removedPyObjectalias →Py<PyAny>,Bound::downcast→Bound::cast, the reshaped two-lifetimeFromPyObjecttrait (extract(Borrowed<…>)replacingextract_bound(&Bound<…>)), and the now-opt-inCloneauto-FromPyObjecton#[pyclass]types (PyVNodeopts in,SupervisorStatsPyopts out). Behavior-preserving: clippy clean under-D warnings, the full Rust suite (two-phase) and the full Python suite both pass against the rebuilt extension; no public API change.
Fixed
Deploy doctor no longer false-positives on env-derived
DATABASESbuilt from individualos.environvars (#1768, follow-up to #1760). The #1760DATABASEScheck warned whenever the settings text mentioned neitherDATABASE_URLnordj_database_url, so a perfectly environment-driven config assembled from individualos.environ['DB_NAME']/['DB_HOST']/… vars got a spurious "doesn't consultDATABASE_URL" warning. The env-read detection is now widened — the canonicalDATABASE_URL/dj_database_urltokens still count anywhere, and other env reads (os.environ,os.getenv, python-decoupleconfig()/env()) count within theDATABASESassignment region (a new brace-balancing_databases_region()scopes it, so an unrelatedSECRET_KEY = os.environ[...]elsewhere can't mask a genuinely hardcoded DB block). 2 new cases inTestDeployDoctor.Log the swallowed theme-context cache-write skip instead of silently passing (#2380). CodeQL flagged
except (AttributeError, TypeError): passintheme_context(python/djust/theming/context_processors.py) as an empty except. The branch intentionally skips the per-request cache write for request objects that can't hold arbitrary attributes (a__slots__object in tests / exotic callers) — correctness over the micro-optimization — but the swallowed write was dropped with no trace. It now emits alogger.debugnaming the request type and the exception, satisfying both CodeQL and the project's no-bare-except rule. Behavior is otherwise unchanged. New case inTestThemeContextCache.djust deploynow respects.gitignorewhen building the deploy tarball, and warns before oversized uploads (#1759)._create_tarballpreviously walked the source tree using a hardcodedEXCLUDE_*list, ignoring the project's.gitignore. Non-standard names — a.venv-dev/virtualenv carrying a 115 MB Playwrightnodebinary,scratch/screenshots,mobile-app/wheels/— slipped straight in, producing a 152 MB tarball that the server's ingress rejected with a raw 413. Fix: when the source is a git work tree,_create_tarballtakes its file list fromgit ls-files --cached --others --exclude-standard(tracked ∪ untracked, minus ignored), so the project's.gitignoreis the source of truth. Non-git directories fall back to the existingos.walkpath unchanged. TheEXCLUDE_*security net (EXCLUDE_DIR_NAMES,EXCLUDE_FILENAMES,EXCLUDE_FILE_SUFFIXES,EXCLUDE_FILENAME_STEMS) still applies on the git path so credentials and live databases are dropped even if the user forgot to gitignore them (#1505 intent preserved). A new_tarball_size_warningprints an actionable warning to stderr before upload when the packed tarball exceeds 50 MB, listing the largest included files so the user can identify what to add to.gitignoreinstead of hitting a raw nginx 413 page. 6 new regression cases inpython/tests/test_deploy_cli.py.