diff --git a/src/proxy/cache/redis-cache.ts b/src/proxy/cache/redis-cache.ts index fe758a8d05..83347397d0 100644 --- a/src/proxy/cache/redis-cache.ts +++ b/src/proxy/cache/redis-cache.ts @@ -16,6 +16,9 @@ export class RedisCache implements TokenCache { private readonly prefix: string; private readonly url: string; private readonly connectTimeout: number; + private readonly tls: boolean; + private readonly password?: string; + private readonly username?: string; private hits = 0; private misses = 0; private connected = false; @@ -24,6 +27,9 @@ export class RedisCache implements TokenCache { this.url = options.url; this.prefix = options.prefix ?? DEFAULT_PREFIX; this.connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS; + this.tls = options.tls ?? options.url.startsWith("rediss://"); + this.password = options.password; + this.username = options.username; } private key(k: string): string { @@ -212,18 +218,24 @@ export class RedisCache implements TokenCache { return withSpan("cache.redis.connect", async () => { if (this.connected && this.client) return; - const client = createClient({ + // deno-lint-ignore no-explicit-any + const clientOpts: Record = { url: this.url, socket: { connectTimeout: this.connectTimeout, - reconnectStrategy: (retries) => { + tls: this.tls || undefined, + reconnectStrategy: (retries: number) => { if (retries > MAX_RECONNECT_RETRIES) { return new Error("Max reconnection attempts reached"); } return Math.min(retries * RECONNECT_BACKOFF_BASE_MS, RECONNECT_BACKOFF_MAX_MS); }, }, - }); + }; + if (this.password) clientOpts.password = this.password; + if (this.username) clientOpts.username = this.username; + + const client = createClient(clientOpts); client.on("error", (err) => { logger.error("[RedisCache] Client error", { diff --git a/src/proxy/cache/types.ts b/src/proxy/cache/types.ts index ce34ceb5d3..28f4fc68f6 100644 --- a/src/proxy/cache/types.ts +++ b/src/proxy/cache/types.ts @@ -38,6 +38,9 @@ export interface RedisCacheOptions { url: string; prefix?: string; connectTimeout?: number; + tls?: boolean; + password?: string; + username?: string; } export type CacheOptions = diff --git a/src/utils/redis-client.ts b/src/utils/redis-client.ts index ce7742a1a0..cc393e8938 100644 --- a/src/utils/redis-client.ts +++ b/src/utils/redis-client.ts @@ -19,10 +19,13 @@ export interface RedisClient { isOpen?: boolean; } -interface RedisClientOptions { +export interface RedisClientOptions { url?: string; connectTimeout?: number; autoReconnect?: boolean; + tls?: boolean; + password?: string; + username?: string; } let sharedClient: RedisClient | null = null; @@ -64,12 +67,14 @@ export async function getRedisClient(options: RedisClientOptions = {}): Promise< } async function createClient(options: RedisClientOptions): Promise { - let createClientFn: ((opts: { url?: string }) => RedisClient) | undefined; + // deno-lint-ignore no-explicit-any + let createClientFn: ((opts: Record) => RedisClient) | undefined; try { const redisClientModule = "npm:@redis/client@1.5.8"; const mod = await import(redisClientModule); - createClientFn = mod.createClient as (opts: { url?: string }) => RedisClient; + // deno-lint-ignore no-explicit-any + createClientFn = mod.createClient as (opts: Record) => RedisClient; } catch (error) { logger.debug("Failed to load @redis/client module", { error }); throw DEPENDENCY_MISSING.create({ @@ -78,7 +83,30 @@ async function createClient(options: RedisClientOptions): Promise { }); } - const client = createClientFn({ url: options.url ?? getEnv("REDIS_URL") }); + const url = options.url ?? getEnv("REDIS_URL"); + const useTls = options.tls ?? url?.startsWith("rediss://") ?? false; + + if (!useTls && getEnv("NODE_ENV") === "production") { + logger.warn( + "Redis connection without TLS in production. Set REDIS_URL to rediss:// or pass tls: true.", + ); + } + + // deno-lint-ignore no-explicit-any + const clientOpts: Record = { url }; + if (useTls) { + clientOpts.socket = { tls: true }; + } + const password = options.password ?? getEnv("REDIS_PASSWORD"); + if (password) { + clientOpts.password = password; + } + const username = options.username ?? getEnv("REDIS_USERNAME"); + if (username) { + clientOpts.username = username; + } + + const client = createClientFn(clientOpts); if (typeof client.on === "function") { client.on("error", (err: unknown) => {