| 400 | }; |
| 401 | |
| 402 | const checkOrigins = (env: Env): string[] => { |
| 403 | if (env.NODE_ENV !== "production") { |
| 404 | return []; |
| 405 | } |
| 406 | |
| 407 | /* |
| 408 | * Empty ALLOWED_ORIGINS is valid in production: it signals a same-origin |
| 409 | * deployment (BoringStack's default — Traefik path-routes /api/* on the |
| 410 | * same host that serves the SPA). The CORS middleware is then not mounted. |
| 411 | * If it IS set, every entry must be HTTPS and not a wildcard. |
| 412 | */ |
| 413 | if (env.ALLOWED_ORIGINS.length === 0) { |
| 414 | return []; |
| 415 | } |
| 416 | |
| 417 | const hasInvalid = env.ALLOWED_ORIGINS.some((origin) => { |
| 418 | if (origin.includes("*")) { |
| 419 | return true; |
| 420 | } |
| 421 | |
| 422 | try { |
| 423 | return new URL(origin).protocol !== "https:"; |
| 424 | } catch { |
| 425 | return true; |
| 426 | } |
| 427 | }); |
| 428 | |
| 429 | return hasInvalid |
| 430 | ? ["Production ALLOWED_ORIGINS must be HTTPS, no wildcards"] |
| 431 | : []; |
| 432 | }; |
| 433 | |
| 434 | /** |
| 435 | * Catches the most common deploy-day footgun: shipping the template's |