# PiCloud dev Caddyfile.
#
# Routing topology:
#   /healthz, /version        → picloud (liveness + version, k8s-friendly)
#   /api/v1/admin/*           → picloud (manager control plane)
#   /api/v1/execute/{id}      → picloud (orchestrator ID-based bypass)
#   /api/*                    → 404      (unsupported / sunset API version)
#   /admin/*                  → dashboard SPA  (mounted with paths.base=/admin)
#   everything else           → picloud (orchestrator route table; user paths)
#
# The "everything else → picloud" rule is what makes user-defined paths
# like /greet possible. picloud's matcher answers them via the routes
# table, or returns 404 with a precise JSON error.
#
# When v2 of the API ships, add `handle /api/v2/admin/* { ... }` etc.
# alongside the v1 handles, before the catch-all `/api/*` 404.
#
# Audit 2026-06-11 (H-A1/2/3, M07-06/07/08/09): defense-in-depth headers.
# CSP / X-Frame-Options / Permissions-Policy / no-store land on the
# dashboard SPA and admin API; nosniff + Referrer-Policy land everywhere.
# User-route responses (catch-all `handle`) deliberately get NO CSP —
# user scripts own their own response headers by design.
# `?` operator = "default if missing" so a downstream response (e.g. the
# file-download endpoint with its own restrictive CSP) wins.
{
	auto_https off
	admin off
	log {
		output stdout
		format console
	}
}

:80 {
	# Audit 2026-06-11 (H-1) — hard request-body ceiling at the proxy.
	# Without this, Caddy forwards bodies up to its multi-GB default to
	# picloud. 12 MB sits just above the orchestrator's 10 MiB user-route
	# read so legitimate large invokes pass; admin / email handlers are
	# bounded far tighter by their own Axum extractor limits.
	request_body {
		max_size 12MB
	}

	# Baseline headers applied to every response.
	header {
		X-Content-Type-Options "nosniff"
		Referrer-Policy "no-referrer"
	}

	handle /healthz {
		reverse_proxy picloud:8080
	}
	handle /version {
		reverse_proxy picloud:8080
	}

	handle /api/v1/admin/* {
		header {
			?Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
			?X-Frame-Options "DENY"
			?Cache-Control "no-store"
			?Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), accelerometer=(), gyroscope=()"
		}
		reverse_proxy picloud:8080
	}
	handle /api/v1/execute/* {
		reverse_proxy picloud:8080
	}

	handle /api/* {
		respond 404 {
			body "{\"error\":\"no such API version — see /version for supported routes\"}"
			close
		}
	}

	# Dashboard SPA at /admin. Its internal Caddy serves files from /srv
	# (root); we don't strip the prefix because SvelteKit was built with
	# paths.base = '/admin' so the bundle's URLs already include it.
	handle /admin/* {
		header {
			?Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
			?X-Frame-Options "DENY"
			?Cache-Control "no-store"
			?Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), accelerometer=(), gyroscope=()"
		}
		reverse_proxy dashboard:80
	}
	handle /admin {
		# Bare /admin (no trailing slash) — let SvelteKit's SPA handle it.
		header {
			?Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
			?X-Frame-Options "DENY"
		}
		reverse_proxy dashboard:80
	}

	# Everything else → picloud's user-route fallback. If no route
	# matches, picloud responds 404 with a JSON body.
	handle {
		reverse_proxy picloud:8080
	}

	log {
		output stdout
		format console
	}
}
