/** * Boot-time guard for the Redis connection string. Same family as * `check-database-url.ts` / `check-entra-config.ts` / * `check-session-secret.ts` — fails before NestFactory if the URL * is missing or obviously malformed, so the contributor sees one * clear message instead of a `ioredis` reconnect storm. * * v1 understands the single-instance form (`redis://[user]:pass@host:port/db`). * Sentinel-style configuration (`REDIS_SENTINEL_HOSTS` + * `REDIS_SENTINEL_NAME` per ADR-0010) lands when the production * deploy ADR ships; until then the validator only accepts URLs. */ const PLACEHOLDER_PASSWORD = 'redis_dev_change_me'; export interface RedisConfig { /** Original URL as configured; consumed by `ioredis`'s URL ctor. */ readonly url: string; /** Parsed host (for log lines and the future readiness probe). */ readonly host: string; /** Parsed port (defaults to 6379 when the URL omits it). */ readonly port: number; } export function assertRedisConfig(): RedisConfig { const raw = process.env['REDIS_URL']; if (!raw) { throw new Error( 'REDIS_URL is not set. Copy apps/portal-bff/.env.example to ' + 'apps/portal-bff/.env and adjust to match infra/local/.env ' + '(REDIS_PASSWORD + REDIS_PORT).', ); } let parsed: URL; try { parsed = new URL(raw); } catch { throw new Error( `REDIS_URL is not a valid URL. Expected ` + `"redis://[user]:@:/"; got: ${truncate(raw)}`, ); } if (parsed.protocol !== 'redis:' && parsed.protocol !== 'rediss:') { throw new Error( `REDIS_URL must use the "redis://" or "rediss://" scheme; got "${parsed.protocol}".`, ); } // The local Compose stack requires a non-default password (the // compose refuses to boot without it). A REDIS_URL without one // would only connect to a non-authenticated Redis — surface the // mismatch up-front rather than at the first connection attempt. if (!parsed.password) { throw new Error( `REDIS_URL has no password. The local dev stack requires a ` + `password (REDIS_PASSWORD in infra/local/.env). Add it to ` + `the URL's userinfo segment.`, ); } if (parsed.password === PLACEHOLDER_PASSWORD) { throw new Error( `REDIS_URL still carries the .env.example placeholder password ` + `("${PLACEHOLDER_PASSWORD}"). Set a real value in ` + `infra/local/.env and mirror it here.`, ); } const port = parsed.port ? Number(parsed.port) : 6379; if (!Number.isFinite(port) || port <= 0 || port > 65535) { throw new Error(`REDIS_URL has an invalid port: ${parsed.port}`); } return { url: raw, host: parsed.hostname, port, }; } function truncate(s: string): string { return s.length > 32 ? `${s.slice(0, 32)}…` : s; }