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 customer portal lives on, e.g. app.clupilot.com. | | Empty means the portal answers on every host, which is the default and | what every development machine reached by a bare IP depends on. | | Set, every portal route — sign-in, registration, the dashboard, Fortify's | own POST actions through fortify.domain — is bound to this host and does | not exist anywhere else. www.clupilot.com/dashboard is then a 404 rather | than a working page, which is the point: a route belongs to a hostname, | and a route that is not registered for the host being asked cannot answer. | | The first attempt at this bound only the landing page and left the portal | host-agnostic — so the website vanished from the portal, and the portal | stayed reachable from the website. Half a separation is no separation. */ 'app_host' => strtolower(trim((string) env('APP_HOST', ''))), /* | Hostnames the public website lives on, e.g. www.clupilot.com,clupilot.com. | | Comma-separated, and the FIRST is canonical: it serves the site, and every | other name in the list redirects to it permanently, path and all. An apex | domain and its www are two names for one site, and answering on both | without picking one splits the search rankings and the cookies between | them. | | 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 — landing page, robots.txt and the legal pages — exists | on these names and nowhere else. Every other host answers "/" with the | product instead. The console keeps its own "/": its routes are registered | first and win. | | Binding the legal pages is also what makes route('legal.impressum') | produce a canonical URL from inside a queued mail, where there is no | request to take a hostname from. */ 'site_hosts' => array_values(array_filter(array_map( fn ($host) => strtolower(trim($host)), explode(',', (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')), ))), ];