Added
JS Commands — client-side DOM commands chainable from templates, views, hooks, and JavaScript — Closes the single biggest DX gap vs Phoenix LiveView 1.0. Eleven commands (
show,hide,toggle,add_class,remove_class,transition,dispatch,focus,set_attr,remove_attr,push) that run locally without a server round-trip, plus apushescape hatch that mixes in server events when needed. Four equivalent entry points: (1) Python helperdjust.js.JS— fluent chain builder that stringifies to a JSON command list, wrapped inSafeStringfor safe template embedding (<button dj-click="{{ JS.show('#modal').add_class('active', to='#overlay') }}">Open</button>). (2) Client-sidewindow.djust.js— mirror of the Python API withcamelCasemethod names for direct JavaScript use (window.djust.js.show('#modal').addClass('active', {to: '#overlay'}).exec()). (3) Hook API — everydj-hookinstance now has athis.js()method returning a chain bound to the hook element (Phoenix 1.0 parity for programmable JS Commands from hook lifecycle callbacks). (4) Attribute dispatcher —dj-click(and other event-binding attributes) detect whether the attribute value is a JSON command list ([[...]]) and execute it locally; plain handler names still fire server events as before (zero breaking changes). All commands support scoped targets:to=<selector>(absolutedocument.querySelectorAll),inner=<selector>(scoped to origin element's descendants),closest=<selector>(walk up the DOM from origin) — a single<button dj-click="{{ JS.hide(closest='.modal') }}">Close</button>works in every modal with no per-instance IDs. Thepushcommand acceptspage_loading=Trueto show the navigation-level loading bar while the event round-trips. Chains are immutable — every chain method returns a newJSChain, so reusing a base chain across multiple call sites never cross-contaminates. 37 new Python tests (every command + target validation + chain immutability + HTML/SafeString integration + template rendering) and 30 new JS tests (every command executing against real DOM + target resolution + chain fluency + attribute dispatcher + backwards-compat for plain event names +parseCommandValueedge cases). Zero new dependencies — the Python helper is stdlib-only and the JS interpreter is ~350 lines in a newsrc/26-js-commands.jsmodule. Full guide indocs/website/guides/js-commands.mdwith examples for templates, hooks, chaining, and the "when to reach for what" decision tree.dj-paste— paste event handling — New attribute that fires a server event when the user pastes content into a bound element (<textarea dj-paste="handle_paste">). The client extracts structured payload from theClipboardEventin one pass:text(clipboardData.getData('text/plain')),html(getData('text/html')for rich paste from Word/Google Docs/web pages),has_files(bool), andfiles(list of{name, type, size}metadata dicts for every file inclipboardData.files). When the element also carries adj-upload="<slot>"attribute, the clipboard'sFileListis routed through the existing upload pipeline — image-paste → chat, CSV-paste → table, etc. — via a newwindow.djust.uploads.queueClipboardFiles(element, fileList)export. Participates in the standard interaction pipeline (dj-confirm,dj-lock). By default the browser's native paste still happens so hybrid editors feel natural; adddj-paste-suppressto intercept fully (useful when routing image paste to an upload slot without dumping a data URL into a<div contenteditable>). Positional args in the attribute syntax (dj-paste="handle_paste('chat', 42)") forward viakwargs["_args"]. 11 new JS tests covering text extraction, HTML extraction, file metadata, suppress flag, missingclipboardData, double-bind protection, positional args, upload routing with and without adj-uploadslot, and graceful degradation whengetData('text/html')throws. ~80 lines JS. Full guide indocs/website/guides/dj-paste.md.djust_audit --ast— AST security anti-pattern scanner (#660) — Adds a new mode todjust_auditthat walks the project's Python source and Django templates looking for five specific security anti-patterns, each motivated by a live vulnerability or near-miss in the 2026-04-10 a downstream consumer penetration test. Seven stable finding codesdjust.X001–djust.X007: X001 (ERROR) — possible IDOR:Model.objects.get(pk=...)inside a DetailView / LiveView without a sibling.filter(owner=request.user)(oruser=,tenant=,organization=,team=,created_by=,author=,workspace=) scoping the queryset. X002 (WARN) — state-mutating@event_handlerwithout any permission check (no class-levellogin_required/permission_required, no@permission_required/@login_required). X003 (ERROR) — SQL string formatting:.raw()/.extra()/cursor.execute()passed an f-string, a.format()call, or a"..." % ...binary-op. X004 (ERROR) — open redirect:HttpResponseRedirect(request.GET[...])/redirect(...)without anurl_has_allowed_host_and_schemeoris_safe_urlguard in the enclosing function. X005 (ERROR) — unsafemark_safe/SafeStringwrapping an interpolated string (XSS risk). X006 (WARN) — template uses{{ var|safe }}(regex scan of.htmlfiles). X007 (WARN) — template uses{% autoescape off %}. Suppression via# djust: noqa X001on the offending line, or{# djust: noqa X006 #}inside templates. New CLI flags:--ast,--ast-path <dir>,--ast-exclude <prefix> [...],--ast-no-templates. Supports--jsonand--strict(fail on warnings too). 52 new tests covering positive + negative cases for every checker, management-command integration, template scanning, and noqa suppression. Zero new runtime dependencies — stdlibast+re. Full documentation indocs/guides/djust-audit.mdanddocs/guides/error-codes.md#ast-anti-pattern-scanner-findings-x0xx. Closes the v0.4.1 audit-enhancement batch (#657/#659/#660/#661 all shipped).New consolidated
djust_auditcommand guide —docs/guides/djust-audit.mddocuments all five modes of the command (default introspection,--permissions,--dump-permissions,--live,--ast), every CLI flag, CI integration examples, and exit-code conventions. Cross-linked fromdocs/guides/security.md.Error code reference expanded with 44 new codes —
docs/guides/error-codes.mdnow covers the A0xx static audit checks (7 codes: A001, A010, A011, A012, A014, A020, A030), the P0xx permissions-document findings (7 codes: P001–P007), and the L0xx runtime-probe findings (30 codes: L001–L091). Every code gets severity, cause, fix, and a reference to the related issue/PR.{% live_input %}template tag — standalone state-bound form fields for non-Form views (#650) —FormMixin.as_live_field()andWizardMixin.as_live_field()render form fields with proper CSS classes,dj-input/dj-changebindings, and framework-aware styling — but only for views backed by a DjangoFormclass. This leaves non-form views (modals, inline panels, search boxes, settings pages, anywhere state lives directly on view attributes) without an equivalent helper. The new{% live_input %}tag fills this gap with a lightweight alternative that needs noFormclass orWizardMixin. Supports 12 field types (text,textarea,select,password,email,number,url,tel,search,hidden,checkbox,radio), explicitevent=override (defaults sensibly per type —text→dj-input,select/radio/checkbox→dj-change,hidden→ none),debounce=/throttle=passthrough, framework CSS class resolution viaconfig.get_framework_class('field_class'), HTML attribute passthrough with underscore-to-dash normalisation (aria_label="Search"→aria-label="Search"), and a tested XSS escape boundary via a new shareddjust._html.build_tag()helper. Example:{% live_input "text" handler="search" value=query debounce="300" placeholder="Search..." %}. 56 new tests including an explicit XSS matrix across every field type and attribute. Seedocs/guides/live-input.mdfor the full setup guide.djust_audit --live <url>— runtime security-header and CSWSH probe (#661) — Adds a new mode todjust_auditthat fetches a running deployment with stdliburlliband validates security headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP, CORP), cookies (HttpOnly, Secure, SameSite on session/CSRF cookies), information-disclosure paths (/.git/config,/.env,/__debug__/,/robots.txt,/.well-known/security.txt), and optionally probes the WebSocket endpoint withOrigin: https://evil.exampleto verify the CSWSH defense from #653 is actually enforced end-to-end. This catches the class of production issues where the setting is correctly configured insettings.pybut the response is stripped, rewritten, or never emitted by the time it reaches the client — a downstream consumer pentest caught a criticalContent-Security-Policy missingcase this way (django-cspwas configured but the header was absent from production responses, stripped by an nginx ingress). 30 new stable finding codesdjust.L001–djust.L091cover every check class so CI configs can suppress specific codes by number. New CLI flags:--live <url>,--paths(multi-URL),--no-websocket-probe,--header 'Name: Value'(for staging auth),--skip-path-probes(for WAF-protected environments). Supports--jsonand--strict(fail on warnings too). Zero new runtime dependencies — stdliburllibfor HTTP, optionalwebsocketspackage for the WebSocket probe (skipped with an INFO finding if not installed).See
docs/guides/djust-audit.md.New static security checks in
djust_check/djust_audit(#659) — Seven new check IDs fire fromcheck_configurationwhen Django runspython manage.py check: A001 (ERROR) — WebSocket router not wrapped inAllowedHostsOriginValidator(static-analysis companion to #653 for existing apps built from older scaffolds). A010 (ERROR) —ALLOWED_HOSTS = ["*"]in production. A011 (ERROR) —ALLOWED_HOSTSmixes"*"with explicit hosts (the wildcard makes the explicit entries meaningless). A012 (ERROR) —USE_X_FORWARDED_HOST=Truecombined with wildcardALLOWED_HOSTSenables Host header injection. A014 (ERROR) —SECRET_KEYstarts withdjango-insecure-in production (scaffold default not overridden before deployment). A020 (WARNING) —LOGIN_REDIRECT_URLis a single hardcoded path but the project has multiple auth groups/permissions (catches the "every role lands on the same dashboard" anti-pattern). A030 (WARNING) —django.contrib.admininstalled without a known brute-force protection package (django-axes,django-defender, etc.). Each check has essentially zero false-positive risk, has afix_hintpointing at the remediation, and was motivated by the 2026-04-10 a downstream consumer pentest report. Out of scope for this PR: manifest scanning (k8s/helm/docker-compose env blocks) — deferred to a follow-up. Python-levelsettings.pyvalues cover the common case.djust_audit --permissions permissions.yaml— declarative permissions document for CI-level RBAC drift detection (#657) — Adds a new flag todjust_auditthat validates every LiveView against a committed, human-readable YAML document describing the expected auth configuration for each view. CI fails on any deviation (view declared public but has auth in code, permission list mismatch, undeclared view in strict mode, stale declaration, etc.). This closes a structural gap the existing audit couldn't catch:djust_audittoday can tell "no auth" from "some auth", but not thatlogin_required=Trueshould have beenpermission_required=['claims.view_supervisor']. The permissions document IS the ground truth. Seven stable error codes (djust.P001throughdjust.P007) cover every deviation class. Also adds--dump-permissionsto bootstrap a starter YAML from existing code, and--strictto fail CI on any finding. Full documentation indocs/guides/permissions-document.md. Motivated by a downstream consumer pentest finding 10/11 where every view hadlogin_required=Trueset and djust_audit reported them all as protected, but the lowest-privilege authenticated user could ID-walk the entire database.WizardMixinfor multi-step LiveView form wizards — General-purpose mixin managing step navigation, per-step validation, and data collection for guided form flows. Providesnext_step,prev_step,go_to_step,update_step_field,validate_field, andsubmit_wizardevent handlers. Template context includes step indicators, progress, form data/errors, and pre-rendered field HTML viaas_live_field(). Re-validates all steps on submission to guard against tampered WebSocket replays. (#632)See
docs/website/guides/wizards.md.
Security
LOW: Nonce-based CSP support — drop
'unsafe-inline'fromscript-src/style-src— djust's inline<script>and<style>emissions (handler metadata bootstrap inTemplateMixin._inject_handler_metadata,live_sessionroute map inrouting.get_route_map_script, and the PWA template tagsdjust_sw_register,djust_offline_indicator,djust_offline_styles) now readrequest.csp_noncewhen available (set by django-csp whenCSP_INCLUDE_NONCE_INcovers the relevant directive) and emit anonce="..."attribute on the tag. When no nonce is available (django-csp not installed, orCSP_INCLUDE_NONCE_INnot set), the tags emit without a nonce attribute — fully backward compatible with apps still allowing'unsafe-inline'. Apps that want strict CSP can now setCSP_INCLUDE_NONCE_IN = ("script-src", "script-src-elem", "style-src", "style-src-elem")insettings.py, drop'unsafe-inline'fromCSP_SCRIPT_SRC/CSP_STYLE_SRC, and get strict CSP XSS protection across all djust-generated inline content. The PWA tagsdjust_sw_register,djust_offline_indicator, anddjust_offline_stylesnow usetakes_context=Trueto read the request from the template context — they still work with the same template syntax ({% djust_sw_register %}etc.) as long as aRequestContextis used (Django's default for template rendering). Seedocs/guides/security.mdfor the full setup. Reported via external penetration test 2026-04-10 (FINDING-W06). Closes the v0.4.1 security hardening batch (#653 / #654 / #655). (#655)MEDIUM: Gate VDOM patch timing/performance metadata behind
DEBUG/DJUST_EXPOSE_TIMING—LiveViewConsumerpreviously attachedtiming(handler/render/total ms) andperformance(full nested timing tree with handler and phase names) to every VDOM patch response unconditionally, regardless ofsettings.DEBUG. Combined with CSWSH (#653) this let cross-origin attackers observe server-side code-path timings, enabling timing-based code-path differentiation (DB hit vs cache miss, valid vs invalid CSRF), internal handler/phase name disclosure, and load-based DoS scheduling. Now gated on a new helper_should_expose_timing()which returns True only whensettings.DEBUGor the newsettings.DJUST_EXPOSE_TIMINGis True. Upgrade notes: production behavior change — existing clients that consumedresponse.timing/response.performancein production will no longer see those fields; opt in viaDJUST_EXPOSE_TIMING = Truein settings for staging/profiling. The browser debug panel is unaffected (it receives timing via the existing_attach_debug_payloadpath, which is already gated onDEBUG). Reported via external penetration test 2026-04-10. References: CWE-203, CWE-215, OWASP A09:2021. (#654)HIGH: Validate WebSocket Origin header to prevent Cross-Site WebSocket Hijacking (CSWSH) —
LiveViewConsumer.connect()previously accepted the WebSocket handshake without validating theOriginheader, andDjustMiddlewareStackdid not wrap the router in an origin validator. A cross-origin attacker could mount any LiveView and dispatch any event from a victim's browser. Now the consumer rejects disallowed origins with close code 4403 before accepting the handshake, andDjustMiddlewareStackwraps its inner application inchannels.security.websocket.AllowedHostsOriginValidatorby default (defense in depth). Missing Origin is still allowed so non-browser clients (curl, testWebsocketCommunicator) continue to work. Upgrade notes: ensuresettings.ALLOWED_HOSTSdoes NOT contain*in production; if you need to opt out for a specific stack, useDjustMiddlewareStack(inner, validate_origin=False)(not recommended). Reported via external penetration test 2026-04-10. (#653)Enforce
login_requiredon HTTP GET path — Views withlogin_required = Truerendered full HTML to unauthenticated users on the initial HTTP GET. The WebSocket connection was correctly rejected, but the pre-rendered page content was already visible. Now callscheck_view_auth()beforemount()on HTTP GET and returns 302 toLOGIN_URL. Also callshandle_params()aftermount()on HTTP GET to match the WebSocket path's behavior, preventing state flash on URL-param-dependent views. (#636, fixes #633, #634)
Fixed
Prevent
SynchronousOnlyOperationinPerformanceTracker.track_context_size— The tracker calledsys.getsizeof(str(context)), which triggeredQuerySet.__repr__()on any unevaluated querysets in the context dict.__repr__callslist(self[:21]), evaluating the queryset against the database — raisingSynchronousOnlyOperationin the async WebSocket path. Now uses a shallow per-valuegetsizeofsum that does not invoke__repr__/__str__on values, so lazy objects stay lazy. Size estimates are now slightly less precise (don't include recursive inner size) but safe in async contexts. (#651, fixes #649)Apply RemoveChild patches before batched InsertChild in same parent group —
applyPatchesinclient.js:1379-1440was filteringInsertChildpatches out of each parent group and applying them viaDocumentFragmentbefore iterating the group for theRemoveChildpatches in that same parent, violating the top-level Remove → Insert phase order. This was latent for keyed content (monotonic dj-ids meant removes still found targets by ID), but fired for<!--dj-if-->placeholder comments — they have no dj-id (only elements get IDs), so their removes fall back to index-based lookup, and by the time the removes ran, the batched inserts had already prepended the new content and shifted indices. The removes then deleted the just-inserted content, leaving empty tab content on multi-tab views (symptom: a downstream consumer tab switches showing blank content after the first switch). Fix: split each parent group into non-Insert vs Insert lists, apply all non-Insert patches first in their phase-sorted order, then batch the inserts. (#643, fixes #641, closes #642)dj-patchon<a>tags uses href when attribute value is empty — Booleandj-patchon anchor elements (<a href="?tab=docs" dj-patch>) was resolving to the current URL instead of the href destination. Now falls back toel.getAttribute('href')whendj-patchis empty and the element is<a>. (#640)Normalize Model instances in
render_full_templatebefore passing to Rust — Django FK fields are class-level descriptors not present in__dict__. Rust'sFromPyObjectextracts__dict__which hasclaimant_id=1(raw FK int) instead of the related object. Now always callsnormalize_django_value()on pre-serialized context so FK relationships are resolved viagetattr()and traversable with dot notation ({{ claim.claimant.first_name }}). (#639)Render Django Form/BoundField to SafeString HTML in template context —
{{ form.field_name }}rendered as empty string because the Rust renderer extractedForm.__dict__which doesn't contain computedBoundFieldattributes. Now pre-renders Form and BoundField objects to SafeString HTML viawidget.render()in all four code paths (serialization, template serialization, template rendering, and LiveView state sync). (#631, fixes #621)Correct
has_idsattribute name in WebSocket mount response —websocket.pychecked for"data-dj-id="but the Rust renderer emits"dj-id="attributes. This caused_stampDjIds()to be skipped on pre-rendered pages, breaking VDOM patches for large content swaps (e.g. tab switching) while small patches still worked. The SSE path already had the correct check. (#630, fixes #629)Sync input
.valuefrom attribute after innerHTML/VDOM patch — When navigating backward in a multi-step wizard, text input values were not visually restored even though the server sent correct VDOM patches.setAttribute('value', x)only updates the HTML attribute (defaultValue), not the.valueDOM property. Now syncs.valuefrom the attribute inpreserveFormValues(), broadcast patches, andmorphElement(). Skips focused inputs, checkboxes, radios, and file inputs. (#625, fixes #624)