{
	email {$ACME_EMAIL}
	admin off
	persist_config off
	grace_period 30s

	# Cloudflare publishes these ranges at https://www.cloudflare.com/ips/.
	# Re-check them during every security review. Direct clients cannot forge a
	# CF-Connecting-IP/X-Forwarded-For value that Caddy treats as authoritative.
	servers {
		trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
		trusted_proxies_strict
		client_ip_headers CF-Connecting-IP X-Forwarded-For
		max_header_size 32KB
		protocols h1 h2 h3
		strict_sni_host on
		timeouts {
			read_header 10s
		}
	}
}

(winkchat_security_headers) {
	header {
		-Server
		Strict-Transport-Security "max-age=31536000"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		Referrer-Policy "strict-origin-when-cross-origin"
		Permissions-Policy "geolocation=(), browsing-topics=()"
	}
}

(winkchat_new_api_policy) {
	# Stop first-run root-account takeover. Both the UI and the API stay
	# restricted after initialization so a future restore cannot reopen setup.
	@setupOutsideAdmin {
		path /setup* /api/setup*
		not {
			client_ip {$WINKCHAT_ADMIN_CIDR}
		}
	}
	header @setupOutsideAdmin {
		Content-Type "application/json"
		Cache-Control "no-store"
	}
	respond @setupOutsideAdmin `{"success":false,"message":"Setup is restricted by deployment policy"}` 403

	# The public origin never exposes New API's raw registration or email-code
	# endpoints. The branded same-origin BFF performs consent, Origin, live
	# readiness and body validation before calling these routes internally.
	@directRegistrationPost {
		method POST
		path /api/user/register /api/user/register/
	}
	@directRegistrationCode {
		method GET
		path /api/verification /api/verification/
	}
	header @directRegistrationPost {
		Content-Type "application/json"
		Cache-Control "no-store"
	}
	header @directRegistrationCode {
		Content-Type "application/json"
		Cache-Control "no-store"
	}
	respond @directRegistrationPost `{"success":false,"message":"Use the protected registration flow"}` 403
	respond @directRegistrationCode `{"success":false,"message":"Use the protected registration flow"}` 403
	@internalRegistration path /api/winkchat/registration /api/winkchat/registration/*
	header @internalRegistration {
		Content-Type "application/json"
		Cache-Control "no-store"
	}
	respond @internalRegistration `{"success":false,"message":"Not found"}` 404

	@oauthDisabled path /api/oauth /api/oauth/*
	header @oauthDisabled {
		Content-Type "application/json"
		Cache-Control "no-store"
	}
	respond @oauthDisabled `{"success":false,"message":"OAuth is disabled by deployment policy"}` 403

	# Known rc.24 payment quotes, initiators and callbacks are closed by default.
	# Redemption-code top-up (/api/user/topup) remains available.
	@externalPayments path_regexp externalPayments ^/api/(?:(?:stripe|creem|waffo(?:-pancake)?)/.*|user/(?:amount|pay|epay/.*|stripe/.*|creem/.*|waffo(?:-pancake)?/.*)|subscription/(?:balance|epay|stripe|creem|waffo(?:-pancake)?)/.*)$
	header @externalPayments {
		Content-Type "application/json"
		Cache-Control "no-store"
	}
	respond @externalPayments `{"success":false,"message":"Payments are disabled by deployment policy"}` 403
}

(winkchat_theme_defaults) {
	@noThemePreference {
		method GET HEAD
		header Accept *text/html*
		not header Cookie *theme_*
	}
	header @noThemePreference +Set-Cookie "theme_preset=anthropic; Path=/; Max-Age=31536000; SameSite=Lax; Secure"
	header @noThemePreference +Set-Cookie "theme_font=sans; Path=/; Max-Age=31536000; SameSite=Lax; Secure"
	header @noThemePreference +Set-Cookie "theme_radius=lg; Path=/; Max-Age=31536000; SameSite=Lax; Secure"
	header @noThemePreference +Set-Cookie "theme_winkchat_initialized=1; Path=/; Max-Age=31536000; SameSite=Lax; Secure"
}

(winkchat_logo) {
	@winkchatIcon path /winkchat-logo.svg /favicon.svg
	handle @winkchatIcon {
		root * /srv/winkchat
		rewrite * /favicon.svg
		header Cache-Control "public, max-age=86400"
		file_server
	}
}

(winkchat_new_api_upstream) {
	# Dynamic APIs are never cacheable. no-transform also prevents an edge or
	# browser intermediary from buffering or rewriting streamed model output.
	@dashboardApi path /api /api/*
	header @dashboardApi >Cache-Control "no-store"
	@relayApi path_regexp relayApi ^/(?:v1(?:beta)?|pg|mj|suno|kling)(?:/|$)|^/[^/]+/mj(?:/|$)
	header @relayApi {
		>Cache-Control "no-store, no-cache, no-transform"
		>X-Accel-Buffering "no"
	}

	@health path /healthz
	rewrite @health /api/status
	header @health Cache-Control "no-store"
	reverse_proxy new-api:3000 {
		header_up X-Forwarded-For {client_ip}
		header_up -X-Real-IP
		header_up -CF-Connecting-IP
		health_uri /api/status
		health_interval 30s
		health_timeout 5s
		health_status 200
		fail_duration 30s
		max_fails 3
		lb_try_duration 5s
		lb_try_interval 250ms
	}
}

{$WINKCHAT_DOMAIN} {
	encode zstd gzip
	request_body {
		# Keep this equal to MAX_REQUEST_BODY_MB in docker-compose.yml.
		max_size 32MB
	}
	import winkchat_security_headers

	import winkchat_theme_defaults
	import winkchat_logo
	@versionedMedia path_regexp versionedMedia ^/winkchat-media/.+-v[0-9]+\.(?:png|jpe?g|webp|svg)$
	header @versionedMedia >Cache-Control "public, max-age=31536000, immutable"
	@sourceRelease path /winkchat-source/winkchat-new-api-v1.0.0-rc.24-winkchat.6-source.tgz /winkchat-source/winkchat-new-api-v1.0.0-rc.24-winkchat.6-source.tgz.sha256
	header @sourceRelease {
		>Cache-Control "public, max-age=31536000, immutable"
		>Content-Disposition "attachment"
	}

	# New API's built-in login and invitation links target /sign-up. Keep one
	# public registration implementation and preserve the invitation query.
	redir /sign-up /register?{query} 307
	redir /sign-up/ /register?{query} 307

	# The branded public surface uses explicit routes only. Account, billing,
	# dashboard and API traffic continue to use New API below.
	handle / {
		reverse_proxy winkchat-web:3000
	}
	handle /services* {
		reverse_proxy winkchat-web:3000
	}
	handle /status* {
		reverse_proxy winkchat-web:3000
	}
	handle /security* {
		reverse_proxy winkchat-web:3000
	}
	handle /about* {
		reverse_proxy winkchat-web:3000
	}
	handle /claude-code-guide* {
		reverse_proxy winkchat-web:3000
	}
	handle /docs* {
		reverse_proxy winkchat-web:3000
	}
	handle /user-agreement* {
		reverse_proxy winkchat-web:3000
	}
	handle /payment-agreement* {
		reverse_proxy winkchat-web:3000
	}
	handle /privacy-policy* {
		reverse_proxy winkchat-web:3000
	}
	handle /register* {
		reverse_proxy winkchat-web:3000
	}
	handle /onboarding* {
		reverse_proxy winkchat-web:3000
	}
	handle /winkchat-media/* {
		reverse_proxy winkchat-web:3000
	}
	handle @sourceRelease {
		reverse_proxy winkchat-web:3000
	}
	handle /api/gateway-status {
		reverse_proxy winkchat-web:3000
	}
	@registrationBffDisabled {
		path /api/registration/*
		expression `"{$WINKCHAT_REGISTRATION_MODE:disabled}" != "verified"`
	}
	handle @registrationBffDisabled {
		header Cache-Control "no-store"
		respond `{"success":false,"message":"Public registration is disabled by deployment policy"}` 403
	}
	handle /api/registration/* {
		reverse_proxy winkchat-web:3000 {
			# Replace, never append to, visitor-controlled forwarding headers.
			header_up X-Forwarded-For {client_ip}
			header_up -X-Real-IP
			header_up -CF-Connecting-IP
		}
	}
	handle_path /winkchat-assets/* {
		reverse_proxy winkchat-web:3000
	}

	handle {
		# Setup is never exposed on the customer hostname, even to an
		# administrator source address. Administrators use the separately
		# allowlisted administrator hostname for controlled recovery work.
		@setupOnCustomer path /setup* /api/setup*
		header @setupOnCustomer {
			Content-Type "application/json"
			Cache-Control "no-store"
		}
		respond @setupOnCustomer `{"success":false,"message":"Setup is restricted by deployment policy"}` 403

		import winkchat_new_api_policy

		# The customer hostname serves only customer pages and APIs. New API RBAC
		# remains authoritative, while this edge boundary removes the complete
		# administrator surface from the public customer origin.
		@adminPageOnCustomer path /channels /channels/* /models /models/* /redemption-codes /redemption-codes/* /subscriptions /subscriptions/* /system-info /system-info/* /system-settings /system-settings/* /users /users/*
		respond @adminPageOnCustomer "Not Found" 404

		@adminApiOnCustomer path /api/channel /api/channel/* /api/subscription/admin /api/subscription/admin/* /api/option /api/option/* /api/custom-oauth-provider /api/custom-oauth-provider/* /api/performance /api/performance/* /api/ratio_sync /api/ratio_sync/* /api/redemption /api/redemption/* /api/system-task /api/system-task/* /api/system-info /api/system-info/* /api/group /api/group/* /api/prefill_group /api/prefill_group/* /api/vendors /api/vendors/* /api/models /api/models/* /api/deployments /api/deployments/*
		header @adminApiOnCustomer {
			Content-Type "application/json"
			Cache-Control "no-store"
		}
		respond @adminApiOnCustomer `{"success":false,"message":"Not Found"}` 404

		import winkchat_new_api_upstream
	}
}

# The administrator hostname is intentionally a second security boundary. In
# production it must also sit behind a Cloudflare Access application; this CIDR
# gate protects the origin even if Access or DNS is misconfigured.
{$WINKCHAT_ADMIN_DOMAIN} {
	encode zstd gzip
	request_body {
		max_size 32MB
	}
	import winkchat_security_headers

	@adminOutsideBoundary not client_ip {$WINKCHAT_ADMIN_CIDR}
	handle @adminOutsideBoundary {
		header Cache-Control "no-store"
		respond "Forbidden" 403
	}

	import winkchat_theme_defaults
	import winkchat_logo
	handle {
		import winkchat_new_api_policy
		import winkchat_new_api_upstream
	}
}
