# Caddyfile for ui.suaveplan.com — @suaveplan/react-ui registry + Storybook
#
# Serves two static trees off /srv/registry:
#   /r/*         shadcn registry-item JSON manifests
#   /storybook/* hosted Storybook static build
#
# Auto-TLS via Let's Encrypt. TLS 1.2 minimum, HSTS preload-eligible,
# security headers on every response.
{
	# Global options. ACME contact email; auto-HTTPS is on by default.
	email ops@suaveplan.com
	servers {
		protocols h1 h2
	}
}

ui.suaveplan.com {
	root * /srv/registry

	# TLS pinned to modern protocols only. TLS 1.0/1.1 are rejected.
	tls {
		protocols tls1.2 tls1.3
	}

	# Compression.
	encode zstd gzip

	# Security headers applied to every response.
	# HSTS includes preload directive — domain is preload-list eligible
	# after a 90-day soak of the live deployment.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		Referrer-Policy "strict-origin-when-cross-origin"
		Permissions-Policy "interest-cohort=()"
		# Remove server fingerprinting.
		-Server
	}

	# Health probe — always 200, no filesystem touch, no logs noise.
	handle /health {
		respond "ok" 200 {
			close
		}
	}

	# Block any non-ACME .well-known traffic. Caddy's internal ACME
	# handler binds /.well-known/acme-challenge/* before this block runs;
	# everything else under .well-known is denied.
	@wellKnownNotAcme {
		path /.well-known/*
		not path /.well-known/acme-challenge/*
	}
	respond @wellKnownNotAcme 404

	# Registry JSON tree. Public, CORS-open (manifests are static JSON
	# fetched by `npx shadcn` from any origin). Short TTL so updated
	# manifests propagate quickly to consumers; SWR keeps perceived
	# latency low.
	handle_path /r/* {
		root * /srv/registry/r
		header {
			Access-Control-Allow-Origin "*"
			Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
			Access-Control-Allow-Headers "Content-Type"
			Cache-Control "public, max-age=300, stale-while-revalidate=60"
			Content-Type "application/json; charset=utf-8"
		}
		file_server {
			precompressed br gzip
		}
	}

	# Storybook static build. Restrictive CSP — Storybook needs inline
	# styles and inline scripts for its own runtime, but third-party
	# script sources are blocked.
	handle_path /storybook/* {
		root * /srv/registry/storybook
		header {
			Cache-Control "public, max-age=3600, stale-while-revalidate=600"
			Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'"
		}
		try_files {path} {path}/ /index.html
		file_server
	}

	# Catch-all 404 for anything outside /r, /storybook, /health.
	handle {
		respond "Not Found" 404
	}

	# Logs to stdout — `docker logs` captures.
	log {
		output stdout
		format json
		level INFO
	}
}
