array_values(array_filter(array_map( fn ($host) => strtolower(trim($host)), explode(',', (string) env('ADMIN_HOSTS', '')) ))), /* | Whether the console's hostname serves ONLY the console. | | Off by default, and deliberately separate from having a hostname at all: a | development machine lists its own IP in ADMIN_HOSTS so the console works | without DNS, and that same address still has to serve the portal. Switch | it on only where the console really has a hostname to itself. */ 'exclusive' => (bool) env('ADMIN_HOST_EXCLUSIVE', false), /* | Hostname the public status page lives on, e.g. status.clupilot.com. | | Empty means it stays on every host, as it does today. Set, it becomes the | ONLY place /status answers — everywhere else redirects there, so a link | that already exists keeps working instead of hitting the 404 that strict | separation would otherwise produce. */ 'status_host' => strtolower(trim((string) env('STATUS_HOST', ''))), /* | Hostname the public website lives on, e.g. www.clupilot.com. | | Empty means the landing page answers on every host, which is what it did | — including on the portal's own hostname. Somebody who typed | app.clupilot.com got the shop window: marketing copy, a pricing table and | a "sign up" call to action, at the address where their servers are. | | Set, the website is bound to this host alone, and every OTHER host answers | "/" with the product: the dashboard for somebody signed in, the sign-in | page for everybody else. The console keeps its own "/" — its routes are | registered first and win. | | The legal pages are deliberately NOT bound. An imprint has to be reachable | from wherever the reader is standing, and a mail footer links to it from | whichever host sent the mail. */ 'site_host' => strtolower(trim((string) env('SITE_HOST', ''))), /* | Key for VPN configs stored at rest (32 bytes, base64). Separate from | APP_KEY on purpose — see App\Services\Wireguard\ConfigVault. Empty means | storing configs is switched off, and the console says so rather than | quietly writing credentials in the clear. | | Generate with: head -c 32 /dev/urandom | base64 */ 'vpn_config_key' => env('VPN_CONFIG_KEY', ''), /* | The hostname the console answers on INSIDE the tunnel. | | Set, the VPN gateway and resolver are started (compose profile `vpn`) and | issued client configs point their DNS at the hub. Empty, none of that | exists and the VPN is only a management network — which is what an | installation without the extra services should be. | | Deliberately the SAME name the console already uses publicly: the | certificate is bound to the name, not to the address serving it, so | nothing new has to be issued and nothing about the internal network ends | up in a public DNS zone. */ 'vpn_internal_host' => strtolower(trim((string) env('VPN_INTERNAL_HOST', ''))), /* | Whether the tunnel-side gateway is actually READY, not merely wanted. | | The hostname alone is not enough: the deployment refuses to start the | gateway until a certificate for it has been found, and a client config | that names a resolver nobody started takes the device's whole name | resolution down for as long as the tunnel is up. The certificate path is | written by the same step that starts the services, so it is the honest | signal. */ 'vpn_ready' => strtolower(trim((string) env('VPN_INTERNAL_HOST', ''))) !== '' && filter_var(env('VPN_READY', false), FILTER_VALIDATE_BOOLEAN), /* | Key for credentials stored in the console (32 bytes, base64). | | Separate from APP_KEY on purpose: rotating APP_KEY is ordinary | maintenance, and it would otherwise render every stored credential | unreadable — discovered when Stripe stops answering. Empty means storing | credentials is switched off, and the console says so rather than falling | back to a key that gets rotated. | | Generate with: head -c 32 /dev/urandom | base64 */ 'secrets_key' => env('SECRETS_KEY', ''), /* | Networks that count as "us" while the public site is hidden | (PublicSiteGate). The WireGuard management subnet by default, so anyone on | the VPN sees the real site while the outside world sees a placeholder. | | Note this is compared against the CLIENT address, which is only correct | because TrustProxies is configured — behind an untrusted proxy every | request would look like it came from the proxy. */ 'trusted_ranges' => array_values(array_filter(array_map( 'trim', explode(',', (string) env('TRUSTED_RANGES', '10.66.0.0/24,127.0.0.1')), ))), ];