From 4c7e2efad6da19e998ad5ed742488a24cd44ba8b Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Wed, 3 Jun 2026 16:38:46 +0200 Subject: [PATCH 1/9] fix(redis): use Upstash REST client --- apps/web/package.json | 1 + .../experiments/pick-variant.test.ts | 6 +- apps/web/src/lib/redis.ts | 146 +++++++-------- pnpm-lock.yaml | 168 ++++++++++-------- 4 files changed, 167 insertions(+), 154 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index a9b1f0aeab..027ed2c445 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -101,6 +101,7 @@ "@types/js-cookie": "3.0.6", "@types/js-yaml": "4.0.9", "@types/mdx": "2.0.13", + "@upstash/redis": "^1.38.0", "@vercel/functions": "3.4.6", "@vercel/otel": "2.1.2", "@workos-inc/node": "catalog:", diff --git a/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts b/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts index 20dce833db..8ec51627b2 100644 --- a/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts +++ b/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts @@ -20,7 +20,11 @@ const upstreamB = { internal_id: 'partner-checkpoint-b', base_url: 'https://partner.example.com/v1', }; -const redisIt = process.env.REDIS_URL ? it : it.skip; +const redisIt = + (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) || + (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) + ? it + : it.skip; beforeEach(async () => { await cleanupDbForTest(); diff --git a/apps/web/src/lib/redis.ts b/apps/web/src/lib/redis.ts index 6965e5b3ca..f8887aa7fd 100644 --- a/apps/web/src/lib/redis.ts +++ b/apps/web/src/lib/redis.ts @@ -1,28 +1,23 @@ -import { createClient } from 'redis'; +import { Redis } from '@upstash/redis'; import { captureException } from '@sentry/nextjs'; import type { RedisKey } from '@/lib/redis-keys'; -type RedisClient = ReturnType; type RedisOperation = 'get' | 'getdel' | 'set' | 'del'; -type RedisTimeoutPhase = 'connect' | 'command'; +type RedisConfig = { + url: string; + token: string; + source: 'upstash' | 'vercel-kv'; +}; -// TCP handshake + TLS negotiation can take a moment on a cold connection. -// Redis official docs recommend 1-3s for connect (redis.io/docs/latest/develop/clients). -const CONNECT_TIMEOUT_MS = 1_500; - -// Simple GET/SET commands complete in sub-millisecond; anything over 200ms -// means Redis is overloaded or unreachable and we should fail open. +// Simple GET/SET commands should still be fast over REST; anything over 200ms +// means Redis is overloaded or unreachable and callers should fail open. const COMMAND_TIMEOUT_MS = 200; -let client: RedisClient | null = null; -let connectPromise: Promise | null = null; +let client: Redis | null = null; class RedisTimeoutError extends Error { - constructor( - readonly redisTimeoutPhase: RedisTimeoutPhase, - readonly redisTimeoutMs: number - ) { - super(`Redis timeout (${redisTimeoutPhase})`); + constructor(readonly redisTimeoutMs: number) { + super('Redis timeout (command)'); this.name = 'RedisTimeoutError'; } } @@ -31,127 +26,124 @@ function captureRedisOperationException( err: unknown, operation: RedisOperation, key: RedisKey, - c: RedisClient + config: RedisConfig ) { - const timeoutPhase = err instanceof RedisTimeoutError ? err.redisTimeoutPhase : undefined; + const timeoutPhase = err instanceof RedisTimeoutError ? 'command' : undefined; captureException(err, { tags: { service: 'redis', + redis_transport: 'rest', + redis_config_source: config.source, operation, ...(timeoutPhase ? { redis_timeout_phase: timeoutPhase } : {}), }, extra: { key, - client_is_open: c.isOpen, - client_is_ready: c.isReady, redis_timeout_ms: err instanceof RedisTimeoutError ? err.redisTimeoutMs : undefined, }, }); } -function getOrCreateClient(): RedisClient | null { - if (!process.env.REDIS_URL) { - return null; +function getRedisConfig(): RedisConfig | null { + if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) { + return { + url: process.env.UPSTASH_REDIS_REST_URL, + token: process.env.UPSTASH_REDIS_REST_TOKEN, + source: 'upstash', + }; } - if (!client) { - client = createClient({ - url: process.env.REDIS_URL, - socket: { connectTimeout: CONNECT_TIMEOUT_MS }, - }); - client.on('error', err => { - captureException(err, { tags: { service: 'redis' } }); - }); + + if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { + return { + url: process.env.KV_REST_API_URL, + token: process.env.KV_REST_API_TOKEN, + source: 'vercel-kv', + }; } - return client; + + return null; } -async function ensureConnected(c: RedisClient): Promise { - if (c.isReady) return c; - if (!connectPromise) { - connectPromise = c - .connect() - .catch(err => { - captureException(err, { tags: { service: 'redis', operation: 'connect' } }); - throw err; - }) - .finally(() => { - connectPromise = null; - }); +function getOrCreateClient(): { client: Redis; config: RedisConfig } | null { + const config = getRedisConfig(); + if (!config) { + return null; + } + if (!client) { + client = new Redis({ + url: config.url, + token: config.token, + automaticDeserialization: false, + retry: false, + }); } - await connectPromise; - return c; + return { client, config }; } -function withTimeout( - promise: Promise, - ms: number, - redisTimeoutPhase: RedisTimeoutPhase -): Promise { - let timer: ReturnType; +function withTimeout(promise: Promise, ms: number): Promise { + let timer: ReturnType | undefined; return Promise.race([ - promise.finally(() => clearTimeout(timer)), + promise.finally(() => { + if (timer) clearTimeout(timer); + }), new Promise((_, reject) => { - timer = setTimeout(() => reject(new RedisTimeoutError(redisTimeoutPhase, ms)), ms); + timer = setTimeout(() => reject(new RedisTimeoutError(ms)), ms); }), ]); } export async function redisGet(key: RedisKey): Promise { - const c = getOrCreateClient(); - if (!c) return null; + const redis = getOrCreateClient(); + if (!redis) return null; try { - await withTimeout(ensureConnected(c), CONNECT_TIMEOUT_MS, 'connect'); - return await withTimeout(c.get(key), COMMAND_TIMEOUT_MS, 'command'); + return await withTimeout(redis.client.get(key), COMMAND_TIMEOUT_MS); } catch (err) { - captureRedisOperationException(err, 'get', key, c); + captureRedisOperationException(err, 'get', key, redis.config); throw err; } } export async function redisGetDel(key: RedisKey): Promise { - const c = getOrCreateClient(); - if (!c) return null; + const redis = getOrCreateClient(); + if (!redis) return null; try { - await withTimeout(ensureConnected(c), CONNECT_TIMEOUT_MS, 'connect'); - return await withTimeout(c.getDel(key), COMMAND_TIMEOUT_MS, 'command'); + return await withTimeout(redis.client.getdel(key), COMMAND_TIMEOUT_MS); } catch (err) { - captureRedisOperationException(err, 'getdel', key, c); + captureRedisOperationException(err, 'getdel', key, redis.config); throw err; } } -/** Returns false if Redis is not configured (REDIS_URL unset). */ +/** Returns false if Redis REST env vars are not configured. */ export async function redisSet( key: RedisKey, value: string, ttlSeconds?: number ): Promise { - const c = getOrCreateClient(); - if (!c) return false; + const redis = getOrCreateClient(); + if (!redis) return false; try { - await withTimeout(ensureConnected(c), CONNECT_TIMEOUT_MS, 'connect'); if (ttlSeconds) { - await withTimeout(c.set(key, value, { EX: ttlSeconds }), COMMAND_TIMEOUT_MS, 'command'); + await withTimeout(redis.client.set(key, value, { ex: ttlSeconds }), COMMAND_TIMEOUT_MS); } else { - await withTimeout(c.set(key, value), COMMAND_TIMEOUT_MS, 'command'); + await withTimeout(redis.client.set(key, value), COMMAND_TIMEOUT_MS); } return true; } catch (err) { - captureRedisOperationException(err, 'set', key, c); + captureRedisOperationException(err, 'set', key, redis.config); throw err; } } -/** Returns false if Redis is not configured (REDIS_URL unset). */ +/** Returns false if Redis REST env vars are not configured. */ export async function redisDel(key: RedisKey): Promise { - const c = getOrCreateClient(); - if (!c) return false; + const redis = getOrCreateClient(); + if (!redis) return false; try { - await withTimeout(ensureConnected(c), CONNECT_TIMEOUT_MS, 'connect'); - await withTimeout(c.del(key), COMMAND_TIMEOUT_MS, 'command'); + await withTimeout(redis.client.del(key), COMMAND_TIMEOUT_MS); return true; } catch (err) { - captureRedisOperationException(err, 'del', key, c); + captureRedisOperationException(err, 'del', key, redis.config); throw err; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1778723bab..8b9f9b3f6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -709,6 +709,9 @@ importers: '@types/mdx': specifier: 2.0.13 version: 2.0.13 + '@upstash/redis': + specifier: ^1.38.0 + version: 1.38.0 '@vercel/functions': specifier: 3.4.6 version: 3.4.6(@aws-sdk/credential-provider-web-identity@3.972.43) @@ -771,7 +774,7 @@ importers: version: 14.25.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) emoji-mart: specifier: 5.6.0 version: 5.6.0 @@ -985,7 +988,7 @@ importers: version: link:../encryption drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) zod: specifier: 'catalog:' version: 4.4.3 @@ -1013,7 +1016,7 @@ importers: version: link:../kiloclaw-instance-tiers drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) pg: specifier: 8.20.0 version: 8.20.0 @@ -1204,7 +1207,7 @@ importers: version: 8.13.0 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) stripe: specifier: 'catalog:' version: 19.3.1(@types/node@25.5.2) @@ -1272,7 +1275,7 @@ importers: version: 1.0.20 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -1300,7 +1303,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -1343,7 +1346,7 @@ importers: version: 8.0.3 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) jsonwebtoken: specifier: 'catalog:' version: 9.0.3 @@ -1460,7 +1463,7 @@ importers: version: 11.17.0(typescript@5.9.3) drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) jsonwebtoken: specifier: 'catalog:' version: 9.0.3 @@ -1533,7 +1536,7 @@ importers: version: 11.17.0(typescript@5.9.3) drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -1812,7 +1815,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -1864,7 +1867,7 @@ importers: version: 11.17.0(typescript@5.9.3) drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -1959,7 +1962,7 @@ importers: version: 22.0.1 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) zod: specifier: 'catalog:' version: 4.4.3 @@ -2076,7 +2079,7 @@ importers: version: 0.5.4 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -2174,7 +2177,7 @@ importers: version: 0.9.5 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -2223,7 +2226,7 @@ importers: version: 4.1.0 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) jose: specifier: 'catalog:' version: 6.2.3 @@ -2257,7 +2260,7 @@ importers: version: link:../../packages/db drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) node-html-markdown: specifier: 2.0.0 version: 2.0.0 @@ -2347,7 +2350,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) zod: specifier: 'catalog:' version: 4.4.3 @@ -2384,7 +2387,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) expo-server-sdk: specifier: 6.1.0 version: 6.1.0(patch_hash=7850520582b5b394397b35d1ea195192fe78589d8a6a748fe15177b818c4ed0b) @@ -2427,7 +2430,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -2467,7 +2470,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) workers-tagged-logger: specifier: 'catalog:' version: 1.0.0 @@ -2501,7 +2504,7 @@ importers: version: link:../../packages/worker-utils drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) zod: specifier: 'catalog:' version: 4.4.3 @@ -2538,7 +2541,7 @@ importers: version: 0.0.22 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -2651,7 +2654,7 @@ importers: version: 10.0.1 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) + version: 0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0) hono: specifier: 4.12.18 version: 4.12.18 @@ -9093,6 +9096,9 @@ packages: cpu: [x64] os: [win32] + '@upstash/redis@1.38.0': + resolution: {integrity: sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg==} + '@vercel/functions@3.4.6': resolution: {integrity: sha512-ljfB7SceggUOFjagEmw3PBLSAGQK1NmBln4FV/Cg9cewfbdfJZS88LgC7LRfZ9GgG1kWimkwS/8SLHVtx6lhLA==} engines: {node: '>= 20'} @@ -15756,6 +15762,9 @@ packages: resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} engines: {node: '>=14'} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -18136,9 +18145,9 @@ snapshots: '@chromaui/rrweb-snapshot': 2.0.0-alpha.18-noAbsolute '@playwright/test': 1.58.2 '@segment/analytics-node': 2.1.3 - '@storybook/addon-essentials': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-essentials': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) '@storybook/csf': 0.1.13 - '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) '@storybook/server-webpack5': 8.5.8(@swc/core@1.15.18)(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 @@ -18164,9 +18173,9 @@ snapshots: '@chromaui/rrweb-snapshot': 2.0.0-alpha.18-noAbsolute '@playwright/test': 1.58.2 '@segment/analytics-node': 2.1.3 - '@storybook/addon-essentials': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-essentials': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) '@storybook/csf': 0.1.13 - '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) '@storybook/server-webpack5': 8.5.8(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))(typescript@5.9.3) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 @@ -18259,7 +18268,7 @@ snapshots: cjs-module-lexer: 1.4.3 esbuild: 0.27.4 miniflare: 4.20260508.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) wrangler: 4.90.1(@cloudflare/workers-types@4.20260511.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 transitivePeerDependencies: @@ -23428,7 +23437,7 @@ snapshots: '@stitches/core@1.2.8': {} - '@storybook/addon-actions@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-actions@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 @@ -23437,26 +23446,26 @@ snapshots: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-backgrounds@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-controls@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: '@storybook/global': 5.0.0 dequal: 2.0.3 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-docs@8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.6) - '@storybook/blocks': 8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/csf-plugin': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/react-dom-shim': 8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/blocks': 8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/csf-plugin': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/react-dom-shim': 8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) react: 19.2.6 react-dom: 19.2.4(react@19.2.6) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23477,23 +23486,23 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': - dependencies: - '@storybook/addon-actions': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/addon-backgrounds': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/addon-controls': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/addon-docs': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/addon-highlight': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/addon-measure': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/addon-outline': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/addon-toolbars': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/addon-viewport': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-essentials@8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + dependencies: + '@storybook/addon-actions': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-backgrounds': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-controls': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-docs': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-highlight': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-measure': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-outline': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-toolbars': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-viewport': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-highlight@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-highlight@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: '@storybook/global': 5.0.0 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23505,13 +23514,13 @@ snapshots: optionalDependencies: react: 19.2.6 - '@storybook/addon-measure@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-measure@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: '@storybook/global': 5.0.0 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-outline@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: '@storybook/global': 5.0.0 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23522,16 +23531,16 @@ snapshots: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 - '@storybook/addon-toolbars@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-toolbars@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) - '@storybook/addon-viewport@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/addon-viewport@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: memoizerific: 1.11.3 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) - '@storybook/blocks@8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/blocks@8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: '@storybook/csf': 0.1.12 '@storybook/icons': 1.6.0(react-dom@19.2.4(react@19.2.6))(react@19.2.6) @@ -23543,7 +23552,7 @@ snapshots: '@storybook/builder-webpack5@8.5.8(@swc/core@1.15.18)(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)': dependencies: - '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) '@types/semver': 7.7.1 browser-assert: 1.2.1 case-sensitive-paths-webpack-plugin: 2.4.0 @@ -23579,7 +23588,7 @@ snapshots: '@storybook/builder-webpack5@8.5.8(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))(typescript@5.9.3)': dependencies: - '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) '@types/semver': 7.7.1 browser-assert: 1.2.1 case-sensitive-paths-webpack-plugin: 2.4.0 @@ -23641,11 +23650,11 @@ snapshots: - uglify-js - webpack-cli - '@storybook/components@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/components@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) - '@storybook/core-webpack@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/core-webpack@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 @@ -23655,7 +23664,7 @@ snapshots: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 - '@storybook/csf-plugin@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/csf-plugin@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) unplugin: 1.16.1 @@ -23680,7 +23689,7 @@ snapshots: react: 19.2.6 react-dom: 19.2.4(react@19.2.6) - '@storybook/manager-api@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/manager-api@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23768,17 +23777,17 @@ snapshots: - uglify-js - webpack-cli - '@storybook/preset-server-webpack@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/preset-server-webpack@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: - '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) '@storybook/global': 5.0.0 - '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) safe-identifier: 0.4.2 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 yaml-loader: 0.8.1 - '@storybook/preview-api@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/preview-api@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23796,7 +23805,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/react-dom-shim@8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: react: 19.2.6 react-dom: 19.2.4(react@19.2.6) @@ -23827,8 +23836,8 @@ snapshots: '@storybook/server-webpack5@8.5.8(@swc/core@1.15.18)(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)': dependencies: '@storybook/builder-webpack5': 8.5.8(@swc/core@1.15.18)(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3) - '@storybook/preset-server-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/preset-server-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) transitivePeerDependencies: - '@rspack/core' @@ -23841,8 +23850,8 @@ snapshots: '@storybook/server-webpack5@8.5.8(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))(typescript@5.9.3)': dependencies: '@storybook/builder-webpack5': 8.5.8(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))(typescript@5.9.3) - '@storybook/preset-server-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/preset-server-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) transitivePeerDependencies: - '@rspack/core' @@ -23853,14 +23862,14 @@ snapshots: - webpack-cli optional: true - '@storybook/server@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/server@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: - '@storybook/components': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/components': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) '@storybook/csf': 0.1.12 '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/preview-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - '@storybook/theming': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/preview-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/theming': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 yaml: 2.8.4 @@ -23895,7 +23904,7 @@ snapshots: - supports-color - ts-node - '@storybook/theming@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@storybook/theming@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -24519,6 +24528,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@upstash/redis@1.38.0': + dependencies: + uncrypto: 0.1.3 + '@vercel/functions@3.4.6(@aws-sdk/credential-provider-web-identity@3.972.43)': dependencies: '@vercel/oidc': 3.3.1 @@ -24551,7 +24564,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) '@vitest/expect@3.2.4': dependencies: @@ -24637,7 +24650,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) '@vitest/utils@3.2.4': dependencies: @@ -26336,11 +26349,12 @@ snapshots: esbuild: 0.27.4 tsx: 4.21.0 - drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0): + drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/pg@8.18.0)(@upstash/redis@1.38.0)(bun-types@1.3.14)(kysely@0.29.2)(pg@8.20.0): optionalDependencies: '@cloudflare/workers-types': 4.20260511.1 '@opentelemetry/api': 1.9.1 '@types/pg': 8.18.0 + '@upstash/redis': 1.38.0 bun-types: 1.3.14 kysely: 0.29.2 pg: 8.20.0 @@ -32810,6 +32824,8 @@ snapshots: unbash@2.2.0: {} + uncrypto@0.1.3: {} + undici-types@6.21.0: {} undici-types@7.16.0: {} From 283a2348ed00520b530f7e5d9770abd87733bffe Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 4 Jun 2026 09:23:25 +0200 Subject: [PATCH 2/9] fix(redis): use local REST proxy in dev --- DEVELOPMENT.md | 4 +++- apps/web/src/lib/redis.ts | 4 ++-- dev/docker-compose.yml | 12 ++++++++++++ dev/local/services.ts | 10 ++++++++-- scripts/dev.sh | 3 +++ 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index bef9592376..eb166bbe31 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -452,7 +452,9 @@ With `auto`, the primary worktree gets offset 0 (default ports), and secondary w `pnpm dev:start` also passes a worktree-local Wrangler service-discovery registry at `.wrangler/dev-registry` into its tmux session. For worktrees with distinct `kilo-dev-*` session names, this allows concurrent offset Worker stacks such as `agents` to use the same local Worker names without resolving bindings to Workers running from sibling worktrees. The absolute registry path is recorded in `dev/logs/manifest.json` for diagnostics. -Infrastructure containers (`postgres` on 5432, `redis` on 6379, `grafana` on 4000) always bind to their fixed host ports regardless of the offset - they are shared services, not per-worktree instances. Concurrent worktrees reuse those containers, and `pnpm dev:stop` leaves them running while another `kilo-dev-*` session remains active. +Infrastructure containers (`postgres` on 5432, `redis` on 6379, `redis-http` on 8079, `grafana` on 4000) always bind to their fixed host ports regardless of the offset - they are shared services, not per-worktree instances. Concurrent worktrees reuse those containers, and `pnpm dev:stop` leaves them running while another `kilo-dev-*` session remains active. + +The Next.js dev script exports local Redis defaults before `next dev` starts: `UPSTASH_REDIS_REST_URL=http://localhost:8079` and `UPSTASH_REDIS_REST_TOKEN=example_token` for the shared `@upstash/redis` REST helper, plus `REDIS_URL=redis://localhost:6379` for Chat SDK state because `@chat-adapter/state-redis` uses the Redis TCP protocol. ## Troubleshooting diff --git a/apps/web/src/lib/redis.ts b/apps/web/src/lib/redis.ts index f8887aa7fd..1dc9db4c17 100644 --- a/apps/web/src/lib/redis.ts +++ b/apps/web/src/lib/redis.ts @@ -114,7 +114,7 @@ export async function redisGetDel(key: RedisKey): Promise { } } -/** Returns false if Redis REST env vars are not configured. */ +/** Returns false if Redis is not configured. */ export async function redisSet( key: RedisKey, value: string, @@ -135,7 +135,7 @@ export async function redisSet( } } -/** Returns false if Redis REST env vars are not configured. */ +/** Returns false if Redis is not configured. */ export async function redisDel(key: RedisKey): Promise { const redis = getOrCreateClient(); if (!redis) return false; diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 537f129834..baef2eee08 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -28,6 +28,18 @@ services: - redis_data:/data restart: unless-stopped + redis-http: + image: hiett/serverless-redis-http:latest + ports: + - '8079:80' + environment: + SRH_MODE: env + SRH_TOKEN: example_token + SRH_CONNECTION_STRING: redis://redis:6379 + depends_on: + - redis + restart: unless-stopped + grafana: image: grafana/grafana-oss:13.0.1 container_name: grafana diff --git a/dev/local/services.ts b/dev/local/services.ts index 7c64cbb0aa..dd9dd39bb9 100644 --- a/dev/local/services.ts +++ b/dev/local/services.ts @@ -72,9 +72,10 @@ type ServiceMeta = { const serviceMeta: Record = { // core - nextjs: { group: 'core', dependsOn: ['postgres', 'redis', 'stripe'] }, + nextjs: { group: 'core', dependsOn: ['postgres', 'redis', 'redis-http', 'stripe'] }, postgres: { group: 'core', dependsOn: [] }, redis: { group: 'core', dependsOn: [] }, + 'redis-http': { group: 'core', dependsOn: ['redis'] }, stripe: { group: 'core', dependsOn: [] }, // cloud-agent 'cloud-agent-next': { @@ -334,7 +335,12 @@ function readWranglerPort(dir: string): number { // Build service definitions from serviceMeta + wrangler.jsonc // --------------------------------------------------------------------------- -const INFRA_PORTS: Record = { postgres: 5432, redis: 6379, grafana: 4000 }; +const INFRA_PORTS: Record = { + postgres: 5432, + redis: 6379, + 'redis-http': 8079, + grafana: 4000, +}; // Docker Compose profile that gates each infra service, if any. Services not // listed here are part of the default profile and start with a plain `up -d`. diff --git a/scripts/dev.sh b/scripts/dev.sh index 5b3ac1fcce..8a0bc7e3a5 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -66,6 +66,9 @@ read_env_value() { } export PORT +export REDIS_URL="${REDIS_URL:-redis://localhost:6379}" +export UPSTASH_REDIS_REST_URL="${UPSTASH_REDIS_REST_URL:-http://localhost:8079}" +export UPSTASH_REDIS_REST_TOKEN="${UPSTASH_REDIS_REST_TOKEN:-example_token}" APP_URL_OVERRIDE="${APP_URL_OVERRIDE:-$(read_env_value APP_URL_OVERRIDE)}" NEXTAUTH_URL="${NEXTAUTH_URL:-$(read_env_value NEXTAUTH_URL)}" NEXT_DEV_HOSTNAME="${NEXT_DEV_HOSTNAME:-0.0.0.0}" From 231edbc28770fa2b0e6cd12842e3f1ab9aa71d2e Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 4 Jun 2026 09:39:32 +0200 Subject: [PATCH 3/9] refactor(redis): remove redisGet helper --- .../app/api/openrouter/[...path]/route.test.ts | 9 ++++++--- .../src/app/api/openrouter/providers/route.ts | 4 ++-- apps/web/src/lib/ai-gateway/abuse-service.ts | 4 ++-- .../lib/ai-gateway/experiments/membership.ts | 4 ++-- .../providers/direct-byok/model-list.ts | 4 ++-- .../providers/gateway-models-cache.ts | 4 ++-- .../lib/ai-gateway/providers/vercel/index.ts | 4 ++-- apps/web/src/lib/blacklist-domains-config.ts | 4 ++-- .../platforms/gitlab/oauth-credentials.test.ts | 6 +++--- .../platforms/gitlab/oauth-credentials.ts | 6 ++++-- apps/web/src/lib/posthog-query.ts | 4 ++-- apps/web/src/lib/redis.ts | 17 ++++++----------- .../routers/admin/blacklist-domains-router.ts | 4 ++-- .../src/routers/admin/gateway-config-router.ts | 4 ++-- 14 files changed, 39 insertions(+), 39 deletions(-) diff --git a/apps/web/src/app/api/openrouter/[...path]/route.test.ts b/apps/web/src/app/api/openrouter/[...path]/route.test.ts index a3393e96b5..0e5ac0f15b 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -8,7 +8,7 @@ import { upstreamRequest } from '@/lib/ai-gateway/providers/upstream-request'; import { getOpenRouterModels } from '@/lib/ai-gateway/providers/gateway-models-cache'; import { emitApiMetricsForResponse } from '@/lib/ai-gateway/o11y/api-metrics.server'; import { accountForMicrodollarUsage } from '@/lib/ai-gateway/llm-proxy-helpers'; -import { redisGet, redisSet } from '@/lib/redis'; +import { redisClient, redisSet } from '@/lib/redis'; import type { Provider } from '@/lib/ai-gateway/providers/types'; jest.mock('next/server', () => { @@ -39,7 +39,10 @@ jest.mock('@/lib/ai-gateway/abuse-service', () => { jest.mock('@/lib/ai-gateway/providers/get-provider'); jest.mock('@/lib/ai-gateway/providers/upstream-request'); jest.mock('@/lib/ai-gateway/providers/gateway-models-cache'); -jest.mock('@/lib/redis'); +jest.mock('@/lib/redis', () => ({ + redisClient: { get: jest.fn() }, + redisSet: jest.fn(), +})); jest.mock('@/lib/ai-gateway/o11y/api-metrics.server', () => ({ emitApiMetricsForResponse: jest.fn(), getToolsAvailable: jest.fn(() => false), @@ -65,7 +68,7 @@ const mockedUpstreamRequest = jest.mocked(upstreamRequest); const mockedGetOpenRouterModels = jest.mocked(getOpenRouterModels); const mockedEmitApiMetricsForResponse = jest.mocked(emitApiMetricsForResponse); const mockedAccountForMicrodollarUsage = jest.mocked(accountForMicrodollarUsage); -const mockedRedisGet = jest.mocked(redisGet); +const mockedRedisGet = jest.mocked(redisClient.get); const mockedRedisSet = jest.mocked(redisSet); const provider = { diff --git a/apps/web/src/app/api/openrouter/providers/route.ts b/apps/web/src/app/api/openrouter/providers/route.ts index f0358d2c6c..023ae8ea16 100644 --- a/apps/web/src/app/api/openrouter/providers/route.ts +++ b/apps/web/src/app/api/openrouter/providers/route.ts @@ -3,12 +3,12 @@ import { NextResponse } from 'next/server'; import { captureException } from '@sentry/nextjs'; import { OpenRouterProvidersResponseSchema } from '@/lib/organizations/organization-types'; import { createCachedFetch } from '@/lib/cached-fetch'; -import { redisGet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { GATEWAY_METADATA_REDIS_KEYS } from '@/lib/redis-keys'; const getProviders = createCachedFetch( async () => { - const raw = await redisGet(GATEWAY_METADATA_REDIS_KEYS.openrouterProviders); + const raw = await redisClient.get(GATEWAY_METADATA_REDIS_KEYS.openrouterProviders); if (raw === null) return null; return OpenRouterProvidersResponseSchema.shape.data.parse(JSON.parse(raw)); }, diff --git a/apps/web/src/lib/ai-gateway/abuse-service.ts b/apps/web/src/lib/ai-gateway/abuse-service.ts index 6d20b80fbc..ee26e974ae 100644 --- a/apps/web/src/lib/ai-gateway/abuse-service.ts +++ b/apps/web/src/lib/ai-gateway/abuse-service.ts @@ -26,7 +26,7 @@ import { } from '@/lib/ai-gateway/providers/openrouter/request-helpers'; import { ProxyErrorType } from '@/lib/proxy-error-types'; import { getAutoFreeCandidates } from '@/lib/ai-gateway/auto-model/resolution'; -import { redisGet, redisSet } from '@/lib/redis'; +import { redisClient, redisSet } from '@/lib/redis'; import { abuseRulesClassificationRedisKey } from '@/lib/redis-keys'; import type { FraudDetectionHeaders } from '@/lib/utils'; import { z } from 'zod'; @@ -315,7 +315,7 @@ export async function getCachedRulesEngineAction( identityKey: string ): Promise { try { - const raw = await redisGet(abuseRulesClassificationRedisKey(identityKey)); + const raw = await redisClient.get(abuseRulesClassificationRedisKey(identityKey)); if (!raw) return null; const action = parseCachedRulesEngineAction(raw); return action !== undefined ? { identityKey, action } : null; diff --git a/apps/web/src/lib/ai-gateway/experiments/membership.ts b/apps/web/src/lib/ai-gateway/experiments/membership.ts index 98403c7568..bd7e199867 100644 --- a/apps/web/src/lib/ai-gateway/experiments/membership.ts +++ b/apps/web/src/lib/ai-gateway/experiments/membership.ts @@ -1,4 +1,4 @@ -import { redisGet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { EXPERIMENTED_PUBLIC_IDS_REDIS_KEY } from '@/lib/redis-keys'; import { createCachedFetch } from '@/lib/cached-fetch'; @@ -17,7 +17,7 @@ const EXPERIMENTED_PUBLIC_IDS_LOCAL_CACHE_TTL_MS = process.env.NODE_ENV === 'tes const getExperimentedPublicIds = createCachedFetch( async () => { try { - const cached = await redisGet(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY); + const cached = await redisClient.get(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY); if (cached === null) return []; return parseStringArray(cached) ?? []; } catch { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/model-list.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/model-list.ts index c4eb83e963..79cee1c03b 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/model-list.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/model-list.ts @@ -4,7 +4,7 @@ import { } from '@/lib/ai-gateway/providers/direct-byok/types'; import type { DirectUserByokInferenceProviderId } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; import { createCachedFetch } from '@/lib/cached-fetch'; -import { redisGet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { directByokModelsRedisKey } from '@/lib/redis-keys'; import type { OpenCodeVariant } from '@kilocode/db/schema-types'; @@ -24,7 +24,7 @@ export function cachedEnhancedDirectByokModelList({ enhanceDirectByokModelList({ recommendedModels, remainingModels: DirectByokModelArraySchema.parse( - JSON.parse((await redisGet(directByokModelsRedisKey(providerId))) ?? '[]') + JSON.parse((await redisClient.get(directByokModelsRedisKey(providerId))) ?? '[]') ), variants, }), diff --git a/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts b/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts index b622afb4dc..7abcb63fc6 100644 --- a/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts +++ b/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts @@ -1,6 +1,6 @@ import { StoredModelSchema, type StoredModel } from '@kilocode/db'; import * as z from 'zod'; -import { redisGet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { createCachedFetch } from '@/lib/cached-fetch'; import { GATEWAY_METADATA_REDIS_KEYS } from '@/lib/redis-keys'; import type { RedisKey } from '@/lib/redis-keys'; @@ -12,7 +12,7 @@ const StoredModelMapSchema = z.record(z.string(), StoredModelSchema); function createStoredModelsFetcher(redisKey: RedisKey, name: string) { return createCachedFetch( async () => { - const raw = JSON.parse((await redisGet(redisKey)) ?? 'null'); + const raw = JSON.parse((await redisClient.get(redisKey)) ?? 'null'); if (!raw || typeof raw !== 'object' || Object.keys(raw).length === 0) { console.debug(`[getGatewayModels] no ${name} models found in Redis`); return {}; diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts index 2f2373e78e..172d7074ad 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts @@ -14,7 +14,7 @@ import type { } from '@/lib/ai-gateway/providers/openrouter/types'; import { isReasoningExplicitlyDisabled } from '@/lib/ai-gateway/providers/openrouter/request-helpers'; import { mapModelIdToVercel } from '@/lib/ai-gateway/providers/vercel/mapModelIdToVercel'; -import { redisGet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { createCachedFetch } from '@/lib/cached-fetch'; import { GatewayPercentageSchema, @@ -27,7 +27,7 @@ import type { AnthropicProviderOptions } from '@ai-sdk/anthropic'; const getVercelRoutingPercentage = createCachedFetch( async () => { - const raw = await redisGet(VERCEL_ROUTING_REDIS_KEY); + const raw = await redisClient.get(VERCEL_ROUTING_REDIS_KEY); if (!raw) return DEFAULT_VERCEL_PERCENTAGE; const { vercel_routing_percentage } = GatewayPercentageSchema.parse(JSON.parse(raw)); return vercel_routing_percentage ?? DEFAULT_VERCEL_PERCENTAGE; diff --git a/apps/web/src/lib/blacklist-domains-config.ts b/apps/web/src/lib/blacklist-domains-config.ts index a05758193f..2f29061729 100644 --- a/apps/web/src/lib/blacklist-domains-config.ts +++ b/apps/web/src/lib/blacklist-domains-config.ts @@ -1,6 +1,6 @@ import 'server-only'; import * as z from 'zod'; -import { redisGet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { getEnvVariable } from '@/lib/dotenvx'; import { createCachedFetch } from '@/lib/cached-fetch'; import { BLACKLIST_DOMAINS_REDIS_KEY } from '@/lib/redis-keys'; @@ -42,7 +42,7 @@ function getEnvFallbackDomains(): string[] { */ export const getBlacklistedDomains = createCachedFetch( async (): Promise => { - const raw = await redisGet(BLACKLIST_DOMAINS_REDIS_KEY); + const raw = await redisClient.get(BLACKLIST_DOMAINS_REDIS_KEY); if (raw) { const parsed = BlacklistDomainsConfigSchema.parse(JSON.parse(raw)); if (parsed.domains.length > 0) { diff --git a/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.test.ts b/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.test.ts index 8adc076439..468a4ce244 100644 --- a/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.test.ts +++ b/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.test.ts @@ -1,15 +1,15 @@ import { beforeEach, describe, expect, test } from '@jest/globals'; import { OAUTH_STATE_TTL_SECONDS } from '@/lib/integrations/oauth-state'; -import { redisGet, redisSet } from '@/lib/redis'; +import { redisClient, redisSet } from '@/lib/redis'; import { gitLabOAuthCredentialsRedisKey } from '@/lib/redis-keys'; import { getGitLabOAuthCredentials, storeGitLabOAuthCredentials } from './oauth-credentials'; jest.mock('@/lib/redis', () => ({ - redisGet: jest.fn(), + redisClient: { get: jest.fn() }, redisSet: jest.fn(), })); -const mockedRedisGet = jest.mocked(redisGet); +const mockedRedisGet = jest.mocked(redisClient.get); const mockedRedisSet = jest.mocked(redisSet); const customCredentials = { diff --git a/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.ts b/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.ts index d00fb7b9e1..96fc508208 100644 --- a/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.ts +++ b/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.ts @@ -3,7 +3,7 @@ import 'server-only'; import { randomBytes } from 'node:crypto'; import { z } from 'zod'; import { OAUTH_STATE_TTL_SECONDS } from '@/lib/integrations/oauth-state'; -import { redisGet, redisSet } from '@/lib/redis'; +import { redisClient, redisSet } from '@/lib/redis'; import { gitLabOAuthCredentialsRedisKey } from '@/lib/redis-keys'; import type { GitLabOAuthCredentials } from './adapter'; @@ -33,7 +33,9 @@ export async function storeGitLabOAuthCredentials( export async function getGitLabOAuthCredentials( credentialRef: string ): Promise { - const rawCredentials = await redisGet(gitLabOAuthCredentialsRedisKey(credentialRef)); + const rawCredentials = await redisClient.get( + gitLabOAuthCredentialsRedisKey(credentialRef) + ); if (!rawCredentials) return null; try { diff --git a/apps/web/src/lib/posthog-query.ts b/apps/web/src/lib/posthog-query.ts index b3c3cc6af2..c86c44adb9 100644 --- a/apps/web/src/lib/posthog-query.ts +++ b/apps/web/src/lib/posthog-query.ts @@ -1,5 +1,5 @@ import { getEnvVariable } from '@/lib/dotenvx'; -import { redisGet, redisSet } from '@/lib/redis'; +import { redisClient, redisSet } from '@/lib/redis'; import { posthogQueryRedisKey } from '@/lib/redis-keys'; import * as z from 'zod'; @@ -81,7 +81,7 @@ export function cachedPosthogQuery(schema: z.ZodType) { const key = posthogQueryRedisKey(name); - const cached = await redisGet(key); + const cached = await redisClient.get(key); if (cached !== null) { const data = parse(name, JSON.parse(cached)); memoryCache.set(name, { value: data, at: Date.now() }); diff --git a/apps/web/src/lib/redis.ts b/apps/web/src/lib/redis.ts index 1dc9db4c17..449c4f76a4 100644 --- a/apps/web/src/lib/redis.ts +++ b/apps/web/src/lib/redis.ts @@ -15,6 +15,12 @@ const COMMAND_TIMEOUT_MS = 200; let client: Redis | null = null; +export const redisClient = Redis.fromEnv({ + automaticDeserialization: false, + retry: false, + signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS), +}); + class RedisTimeoutError extends Error { constructor(readonly redisTimeoutMs: number) { super('Redis timeout (command)'); @@ -92,17 +98,6 @@ function withTimeout(promise: Promise, ms: number): Promise { ]); } -export async function redisGet(key: RedisKey): Promise { - const redis = getOrCreateClient(); - if (!redis) return null; - try { - return await withTimeout(redis.client.get(key), COMMAND_TIMEOUT_MS); - } catch (err) { - captureRedisOperationException(err, 'get', key, redis.config); - throw err; - } -} - export async function redisGetDel(key: RedisKey): Promise { const redis = getOrCreateClient(); if (!redis) return null; diff --git a/apps/web/src/routers/admin/blacklist-domains-router.ts b/apps/web/src/routers/admin/blacklist-domains-router.ts index 47d5e6d306..6a8bc48ce1 100644 --- a/apps/web/src/routers/admin/blacklist-domains-router.ts +++ b/apps/web/src/routers/admin/blacklist-domains-router.ts @@ -1,5 +1,5 @@ import { adminProcedure, createTRPCRouter } from '@/lib/trpc/init'; -import { redisGet, redisSet } from '@/lib/redis'; +import { redisClient, redisSet } from '@/lib/redis'; import { BlacklistDomainsConfigSchema, BlacklistDomainsInputSchema, @@ -22,7 +22,7 @@ const SuspiciousDomainsInputSchema = z async function readConfig(): Promise { try { - const raw = await redisGet(BLACKLIST_DOMAINS_REDIS_KEY); + const raw = await redisClient.get(BLACKLIST_DOMAINS_REDIS_KEY); if (!raw) return DEFAULT_BLACKLIST_DOMAINS_CONFIG; return BlacklistDomainsConfigSchema.parse(JSON.parse(raw)); } catch { diff --git a/apps/web/src/routers/admin/gateway-config-router.ts b/apps/web/src/routers/admin/gateway-config-router.ts index 81782c1a87..aa1683a0d6 100644 --- a/apps/web/src/routers/admin/gateway-config-router.ts +++ b/apps/web/src/routers/admin/gateway-config-router.ts @@ -1,5 +1,5 @@ import { adminProcedure, createTRPCRouter } from '@/lib/trpc/init'; -import { redisGet, redisSet } from '@/lib/redis'; +import { redisClient, redisSet } from '@/lib/redis'; import { GatewayConfigSchema, GatewayConfigInputSchema, @@ -11,7 +11,7 @@ import { TRPCError } from '@trpc/server'; async function readConfig(): Promise { try { - const raw = await redisGet(VERCEL_ROUTING_REDIS_KEY); + const raw = await redisClient.get(VERCEL_ROUTING_REDIS_KEY); if (!raw) return DEFAULT_GATEWAY_CONFIG; return GatewayConfigSchema.parse(JSON.parse(raw)); } catch { From 94469e4081f972610d59766e841f5edff3435078 Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 4 Jun 2026 09:59:09 +0200 Subject: [PATCH 4/9] refactor(redis): remove redisSet helper --- .../api/openrouter/[...path]/route.test.ts | 9 ++++---- apps/web/src/lib/ai-gateway/abuse-service.ts | 4 ++-- .../experiments/pick-variant.test.ts | 10 ++++---- .../providers/direct-byok/sync-direct-byok.ts | 4 ++-- .../providers/openrouter/sync-providers.ts | 4 ++-- .../github/user-authorization-state.test.ts | 13 +++++++---- .../github/user-authorization-state.ts | 6 ++--- .../gitlab/oauth-credentials.test.ts | 13 +++++------ .../platforms/gitlab/oauth-credentials.ts | 6 ++--- apps/web/src/lib/posthog-query.ts | 4 ++-- apps/web/src/lib/redis.ts | 23 +------------------ .../routers/admin/blacklist-domains-router.ts | 4 ++-- .../routers/admin/gateway-config-router.ts | 4 ++-- .../routers/admin/model-experiments-router.ts | 4 ++-- 14 files changed, 44 insertions(+), 64 deletions(-) diff --git a/apps/web/src/app/api/openrouter/[...path]/route.test.ts b/apps/web/src/app/api/openrouter/[...path]/route.test.ts index 0e5ac0f15b..f6bdf422ec 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -8,7 +8,7 @@ import { upstreamRequest } from '@/lib/ai-gateway/providers/upstream-request'; import { getOpenRouterModels } from '@/lib/ai-gateway/providers/gateway-models-cache'; import { emitApiMetricsForResponse } from '@/lib/ai-gateway/o11y/api-metrics.server'; import { accountForMicrodollarUsage } from '@/lib/ai-gateway/llm-proxy-helpers'; -import { redisClient, redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import type { Provider } from '@/lib/ai-gateway/providers/types'; jest.mock('next/server', () => { @@ -40,8 +40,7 @@ jest.mock('@/lib/ai-gateway/providers/get-provider'); jest.mock('@/lib/ai-gateway/providers/upstream-request'); jest.mock('@/lib/ai-gateway/providers/gateway-models-cache'); jest.mock('@/lib/redis', () => ({ - redisClient: { get: jest.fn() }, - redisSet: jest.fn(), + redisClient: { get: jest.fn(), set: jest.fn() }, })); jest.mock('@/lib/ai-gateway/o11y/api-metrics.server', () => ({ emitApiMetricsForResponse: jest.fn(), @@ -69,7 +68,7 @@ const mockedGetOpenRouterModels = jest.mocked(getOpenRouterModels); const mockedEmitApiMetricsForResponse = jest.mocked(emitApiMetricsForResponse); const mockedAccountForMicrodollarUsage = jest.mocked(accountForMicrodollarUsage); const mockedRedisGet = jest.mocked(redisClient.get); -const mockedRedisSet = jest.mocked(redisSet); +const mockedRedisSet = jest.mocked(redisClient.set); const provider = { id: 'openrouter', @@ -163,7 +162,7 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => }); mockedClassifyAbuse.mockResolvedValue(classifyResult(null)); mockedRedisGet.mockResolvedValue(null); - mockedRedisSet.mockResolvedValue(true); + mockedRedisSet.mockResolvedValue('OK'); mockedGetOpenRouterModels.mockResolvedValue( new Set(['nvidia/nemotron-3-super-120b-a12b:free']) ); diff --git a/apps/web/src/lib/ai-gateway/abuse-service.ts b/apps/web/src/lib/ai-gateway/abuse-service.ts index ee26e974ae..8b0cb05910 100644 --- a/apps/web/src/lib/ai-gateway/abuse-service.ts +++ b/apps/web/src/lib/ai-gateway/abuse-service.ts @@ -26,7 +26,7 @@ import { } from '@/lib/ai-gateway/providers/openrouter/request-helpers'; import { ProxyErrorType } from '@/lib/proxy-error-types'; import { getAutoFreeCandidates } from '@/lib/ai-gateway/auto-model/resolution'; -import { redisClient, redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { abuseRulesClassificationRedisKey } from '@/lib/redis-keys'; import type { FraudDetectionHeaders } from '@/lib/utils'; import { z } from 'zod'; @@ -331,7 +331,7 @@ export async function cacheRulesEngineAction(args: { }): Promise { if (!args.rulesEngine) return; try { - await redisSet( + await redisClient.set( abuseRulesClassificationRedisKey(args.identityKey), args.rulesEngine.resolved_action ?? 'none' ); diff --git a/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts b/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts index 8ec51627b2..b43b601025 100644 --- a/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts +++ b/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts @@ -6,7 +6,7 @@ import { model_experiment, model_experiment_variant_version } from '@kilocode/db import { eq } from 'drizzle-orm'; import { isPublicIdExperimented } from './membership'; import { pickModelExperimentVariant } from './pick-variant'; -import { redisDel, redisSet } from '@/lib/redis'; +import { redisClient, redisDel } from '@/lib/redis'; import { EXPERIMENTED_PUBLIC_IDS_REDIS_KEY } from '@/lib/redis-keys'; import type { User } from '@kilocode/db/schema'; @@ -40,8 +40,8 @@ async function clearRoutingCaches() { await redisDel(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY); } -async function seedExperimentedPublicIds(ids: string[]): Promise { - return await redisSet(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY, JSON.stringify(ids)); +async function seedExperimentedPublicIds(ids: string[]): Promise { + return await redisClient.set(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY, JSON.stringify(ids)); } afterEach(async () => { @@ -91,7 +91,7 @@ describe('isPublicIdExperimented', () => { redisIt('returns true when the public id has an active experiment', async () => { await makeActiveExperiment({ publicId: 'partner/preview-iset-active' }); - expect(await seedExperimentedPublicIds(['partner/preview-iset-active'])).toBe(true); + expect(await seedExperimentedPublicIds(['partner/preview-iset-active'])).toBe('OK'); expect(await isPublicIdExperimented('partner/preview-iset-active')).toBe(true); }); @@ -101,7 +101,7 @@ describe('isPublicIdExperimented', () => { }); const caller = await createCallerForUser(admin.id); await caller.admin.modelExperiments.pause({ id: experimentId }); - expect(await seedExperimentedPublicIds(['partner/preview-iset-paused'])).toBe(true); + expect(await seedExperimentedPublicIds(['partner/preview-iset-paused'])).toBe('OK'); expect(await isPublicIdExperimented('partner/preview-iset-paused')).toBe(true); }); }); diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts index a3fa354817..58b9522474 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts @@ -1,7 +1,7 @@ import * as z from 'zod'; import { type DirectByokModel } from '@/lib/ai-gateway/providers/direct-byok/types'; import type { DirectUserByokInferenceProviderId } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; -import { redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { directByokModelsRedisKey } from '@/lib/redis-keys'; const DEFAULT_MAX_COMPLETION_TOKENS = 32_000; @@ -165,7 +165,7 @@ async function syncProvider(fetcher: ProviderFetcher, ctx: SyncContext): Promise }); } - await redisSet(directByokModelsRedisKey(fetcher.providerId), JSON.stringify(models)); + await redisClient.set(directByokModelsRedisKey(fetcher.providerId), JSON.stringify(models)); return models.length; } diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts index 6dd5e1fb04..40229e8cb7 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts @@ -24,7 +24,7 @@ import { logAutoModelChangesForAllOrgs } from '@/lib/organizations/auto-model-ch import type { Provider } from '@/lib/ai-gateway/providers/types'; import type { StoredModel } from '@kilocode/db/schema-types'; import { EndpointsSchema, ModelsSchema } from '@kilocode/db/schema-types'; -import { redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { GATEWAY_METADATA_REDIS_KEYS, type RedisKey } from '@/lib/redis-keys'; import { syncDirectByokModels } from '@/lib/ai-gateway/providers/direct-byok/sync-direct-byok'; import { ATTRIBUTION_HEADERS } from '@/lib/ai-gateway/providers/openrouter/attribution-headers'; @@ -312,7 +312,7 @@ async function mirrorToRedis(values: { if (values.openrouterProviders) { entries.push([GATEWAY_METADATA_REDIS_KEYS.openrouterProviders, values.openrouterProviders]); } - await Promise.all(entries.map(([key, value]) => redisSet(key, JSON.stringify(value)))); + await Promise.all(entries.map(([key, value]) => redisClient.set(key, JSON.stringify(value)))); } /** diff --git a/apps/web/src/lib/integrations/platforms/github/user-authorization-state.test.ts b/apps/web/src/lib/integrations/platforms/github/user-authorization-state.test.ts index a1d40ef85a..c5d6606d79 100644 --- a/apps/web/src/lib/integrations/platforms/github/user-authorization-state.test.ts +++ b/apps/web/src/lib/integrations/platforms/github/user-authorization-state.test.ts @@ -1,27 +1,30 @@ import { createHash } from 'node:crypto'; -import { redisGetDel, redisSet } from '@/lib/redis'; +import { redisClient, redisGetDel } from '@/lib/redis'; import { consumeGitHubUserAuthorizationState, createGitHubUserAuthorizationState, } from './user-authorization-state'; jest.mock('@/lib/redis', () => ({ + redisClient: { set: jest.fn() }, redisGetDel: jest.fn(), - redisSet: jest.fn(), })); const mockedRedisGetDel = jest.mocked(redisGetDel); -const mockedRedisSet = jest.mocked(redisSet); +const mockedRedisSet = jest.mocked(redisClient.set); describe('GitHub user authorization state', () => { beforeEach(() => { jest.clearAllMocks(); - mockedRedisSet.mockResolvedValue(true); + mockedRedisSet.mockResolvedValue('OK'); }); test('round-trips a single-use PKCE verifier bound to the Kilo user', async () => { const created = await createGitHubUserAuthorizationState('user_123'); const codeVerifier = mockedRedisSet.mock.calls[0][1]; + if (typeof codeVerifier !== 'string') { + throw new Error('Expected Redis PKCE value to be a string'); + } mockedRedisGetDel.mockResolvedValueOnce(codeVerifier).mockResolvedValueOnce(null); expect(created.codeChallenge).toBe( @@ -45,7 +48,7 @@ describe('GitHub user authorization state', () => { }); test('fails closed if transient PKCE storage is unavailable', async () => { - mockedRedisSet.mockResolvedValue(false); + mockedRedisSet.mockResolvedValue(null); await expect(createGitHubUserAuthorizationState('user_123')).rejects.toThrow( 'configured transient state storage' diff --git a/apps/web/src/lib/integrations/platforms/github/user-authorization-state.ts b/apps/web/src/lib/integrations/platforms/github/user-authorization-state.ts index 4743a9338a..7ab235951b 100644 --- a/apps/web/src/lib/integrations/platforms/github/user-authorization-state.ts +++ b/apps/web/src/lib/integrations/platforms/github/user-authorization-state.ts @@ -7,7 +7,7 @@ import { OAUTH_STATE_TTL_SECONDS, verifyOAuthState, } from '@/lib/integrations/oauth-state'; -import { redisGetDel, redisSet } from '@/lib/redis'; +import { redisClient, redisGetDel } from '@/lib/redis'; import { githubUserAuthorizationPkceRedisKey } from '@/lib/redis-keys'; const STATE_PREFIX = 'github-user-authorization:'; @@ -26,10 +26,10 @@ export async function createGitHubUserAuthorizationState( ): Promise { const codeVerifier = randomBytes(32).toString('base64url'); const verifierRef = randomBytes(16).toString('base64url'); - const stored = await redisSet( + const stored = await redisClient.set( githubUserAuthorizationPkceRedisKey(verifierRef), codeVerifier, - PKCE_TTL_SECONDS + { ex: PKCE_TTL_SECONDS } ); if (!stored) { throw new Error('GitHub user authorization requires configured transient state storage'); diff --git a/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.test.ts b/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.test.ts index 468a4ce244..bdf8d6f0fe 100644 --- a/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.test.ts +++ b/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.test.ts @@ -1,16 +1,15 @@ import { beforeEach, describe, expect, test } from '@jest/globals'; import { OAUTH_STATE_TTL_SECONDS } from '@/lib/integrations/oauth-state'; -import { redisClient, redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { gitLabOAuthCredentialsRedisKey } from '@/lib/redis-keys'; import { getGitLabOAuthCredentials, storeGitLabOAuthCredentials } from './oauth-credentials'; jest.mock('@/lib/redis', () => ({ - redisClient: { get: jest.fn() }, - redisSet: jest.fn(), + redisClient: { get: jest.fn(), set: jest.fn() }, })); const mockedRedisGet = jest.mocked(redisClient.get); -const mockedRedisSet = jest.mocked(redisSet); +const mockedRedisSet = jest.mocked(redisClient.set); const customCredentials = { clientId: 'gitlab-client-id', @@ -23,7 +22,7 @@ describe('GitLab OAuth credential cache', () => { }); test('stores custom credentials in Redis for the OAuth state lifetime', async () => { - mockedRedisSet.mockResolvedValue(true); + mockedRedisSet.mockResolvedValue('OK'); const credentialRef = await storeGitLabOAuthCredentials(customCredentials); @@ -32,12 +31,12 @@ describe('GitLab OAuth credential cache', () => { expect(mockedRedisSet).toHaveBeenCalledWith( gitLabOAuthCredentialsRedisKey(credentialRef), JSON.stringify(customCredentials), - OAUTH_STATE_TTL_SECONDS + 5 + { ex: OAUTH_STATE_TTL_SECONDS + 5 } ); }); test('returns null when Redis is unavailable for credential storage', async () => { - mockedRedisSet.mockResolvedValue(false); + mockedRedisSet.mockResolvedValue(null); await expect(storeGitLabOAuthCredentials(customCredentials)).resolves.toBeNull(); }); diff --git a/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.ts b/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.ts index 96fc508208..ebf4608d90 100644 --- a/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.ts +++ b/apps/web/src/lib/integrations/platforms/gitlab/oauth-credentials.ts @@ -3,7 +3,7 @@ import 'server-only'; import { randomBytes } from 'node:crypto'; import { z } from 'zod'; import { OAUTH_STATE_TTL_SECONDS } from '@/lib/integrations/oauth-state'; -import { redisClient, redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { gitLabOAuthCredentialsRedisKey } from '@/lib/redis-keys'; import type { GitLabOAuthCredentials } from './adapter'; @@ -21,10 +21,10 @@ export async function storeGitLabOAuthCredentials( credentials: GitLabOAuthCredentials ): Promise { const credentialRef = randomBytes(GITLAB_OAUTH_CREDENTIAL_REF_BYTES).toString('base64url'); - const stored = await redisSet( + const stored = await redisClient.set( gitLabOAuthCredentialsRedisKey(credentialRef), JSON.stringify(credentials), - GITLAB_OAUTH_CREDENTIALS_TTL_SECONDS + { ex: GITLAB_OAUTH_CREDENTIALS_TTL_SECONDS } ); return stored ? credentialRef : null; diff --git a/apps/web/src/lib/posthog-query.ts b/apps/web/src/lib/posthog-query.ts index c86c44adb9..f7993716ae 100644 --- a/apps/web/src/lib/posthog-query.ts +++ b/apps/web/src/lib/posthog-query.ts @@ -1,5 +1,5 @@ import { getEnvVariable } from '@/lib/dotenvx'; -import { redisClient, redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { posthogQueryRedisKey } from '@/lib/redis-keys'; import * as z from 'zod'; @@ -98,7 +98,7 @@ export function cachedPosthogQuery(schema: z.ZodType) { `[cachedPosthogQuery] ${name} returned ${data.length} rows in ${performance.now() - startTime}ms` ); - await redisSet(key, JSON.stringify(response.body.results), CACHE_TTL_SECONDS); + await redisClient.set(key, JSON.stringify(response.body.results), { ex: CACHE_TTL_SECONDS }); memoryCache.set(name, { value: data, at: Date.now() }); return data; diff --git a/apps/web/src/lib/redis.ts b/apps/web/src/lib/redis.ts index 449c4f76a4..6971c7c2c7 100644 --- a/apps/web/src/lib/redis.ts +++ b/apps/web/src/lib/redis.ts @@ -2,7 +2,7 @@ import { Redis } from '@upstash/redis'; import { captureException } from '@sentry/nextjs'; import type { RedisKey } from '@/lib/redis-keys'; -type RedisOperation = 'get' | 'getdel' | 'set' | 'del'; +type RedisOperation = 'getdel' | 'del'; type RedisConfig = { url: string; token: string; @@ -109,27 +109,6 @@ export async function redisGetDel(key: RedisKey): Promise { } } -/** Returns false if Redis is not configured. */ -export async function redisSet( - key: RedisKey, - value: string, - ttlSeconds?: number -): Promise { - const redis = getOrCreateClient(); - if (!redis) return false; - try { - if (ttlSeconds) { - await withTimeout(redis.client.set(key, value, { ex: ttlSeconds }), COMMAND_TIMEOUT_MS); - } else { - await withTimeout(redis.client.set(key, value), COMMAND_TIMEOUT_MS); - } - return true; - } catch (err) { - captureRedisOperationException(err, 'set', key, redis.config); - throw err; - } -} - /** Returns false if Redis is not configured. */ export async function redisDel(key: RedisKey): Promise { const redis = getOrCreateClient(); diff --git a/apps/web/src/routers/admin/blacklist-domains-router.ts b/apps/web/src/routers/admin/blacklist-domains-router.ts index 6a8bc48ce1..4789bbc36b 100644 --- a/apps/web/src/routers/admin/blacklist-domains-router.ts +++ b/apps/web/src/routers/admin/blacklist-domains-router.ts @@ -1,5 +1,5 @@ import { adminProcedure, createTRPCRouter } from '@/lib/trpc/init'; -import { redisClient, redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { BlacklistDomainsConfigSchema, BlacklistDomainsInputSchema, @@ -47,7 +47,7 @@ export const adminBlacklistDomainsRouter = createTRPCRouter({ updated_by: ctx.user.id, updated_by_email: ctx.user.google_user_email, }; - const written = await redisSet(BLACKLIST_DOMAINS_REDIS_KEY, JSON.stringify(config)); + const written = await redisClient.set(BLACKLIST_DOMAINS_REDIS_KEY, JSON.stringify(config)); if (!written) { throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', diff --git a/apps/web/src/routers/admin/gateway-config-router.ts b/apps/web/src/routers/admin/gateway-config-router.ts index aa1683a0d6..53990e427b 100644 --- a/apps/web/src/routers/admin/gateway-config-router.ts +++ b/apps/web/src/routers/admin/gateway-config-router.ts @@ -1,5 +1,5 @@ import { adminProcedure, createTRPCRouter } from '@/lib/trpc/init'; -import { redisClient, redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { GatewayConfigSchema, GatewayConfigInputSchema, @@ -32,7 +32,7 @@ export const adminGatewayConfigRouter = createTRPCRouter({ updated_by_email: ctx.user.google_user_email, note: input.note, }; - const written = await redisSet(VERCEL_ROUTING_REDIS_KEY, JSON.stringify(config)); + const written = await redisClient.set(VERCEL_ROUTING_REDIS_KEY, JSON.stringify(config)); if (!written) { throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', diff --git a/apps/web/src/routers/admin/model-experiments-router.ts b/apps/web/src/routers/admin/model-experiments-router.ts index eb6337994c..129a3eabea 100644 --- a/apps/web/src/routers/admin/model-experiments-router.ts +++ b/apps/web/src/routers/admin/model-experiments-router.ts @@ -19,7 +19,7 @@ import { KILOCLAW_KILO_PROVIDER_PREFIX, KILOCODE_KILO_PROVIDER_PREFIX, } from '@/lib/ai-gateway/model-utils'; -import { redisSet } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { TRPCError } from '@trpc/server'; import { and, asc, count, desc, eq, inArray, sql } from 'drizzle-orm'; import * as z from 'zod'; @@ -84,7 +84,7 @@ async function recomputeExperimentedPublicIds() { .from(model_experiment) .where(inArray(model_experiment.status, ROUTING_STATUSES)); const ids = Array.from(new Set(rows.map(r => r.public_model_id))).sort(); - await redisSet(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY, JSON.stringify(ids)); + await redisClient.set(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY, JSON.stringify(ids)); } /** From d9af4426b7b2d12ca36ed086fdc67b94fb1a2750 Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 4 Jun 2026 10:04:19 +0200 Subject: [PATCH 5/9] refactor(redis): remove delete helpers --- .../experiments/pick-variant.test.ts | 6 +- .../github/user-authorization-state.test.ts | 7 +- .../github/user-authorization-state.ts | 4 +- apps/web/src/lib/redis.ts | 116 +----------------- 4 files changed, 10 insertions(+), 123 deletions(-) diff --git a/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts b/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts index b43b601025..5e8dbe60f9 100644 --- a/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts +++ b/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts @@ -6,7 +6,7 @@ import { model_experiment, model_experiment_variant_version } from '@kilocode/db import { eq } from 'drizzle-orm'; import { isPublicIdExperimented } from './membership'; import { pickModelExperimentVariant } from './pick-variant'; -import { redisClient, redisDel } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { EXPERIMENTED_PUBLIC_IDS_REDIS_KEY } from '@/lib/redis-keys'; import type { User } from '@kilocode/db/schema'; @@ -37,7 +37,7 @@ beforeEach(async () => { async function clearRoutingCaches() { // Tests share the dev Redis instance across runs; flush the membership key // so each test sees a fresh load path. - await redisDel(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY); + await redisClient.del(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY); } async function seedExperimentedPublicIds(ids: string[]): Promise { @@ -45,7 +45,7 @@ async function seedExperimentedPublicIds(ids: string[]): Promise } afterEach(async () => { - await redisDel(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY); + await redisClient.del(EXPERIMENTED_PUBLIC_IDS_REDIS_KEY); }); async function makeActiveExperiment(opts: { diff --git a/apps/web/src/lib/integrations/platforms/github/user-authorization-state.test.ts b/apps/web/src/lib/integrations/platforms/github/user-authorization-state.test.ts index c5d6606d79..0063cc3abe 100644 --- a/apps/web/src/lib/integrations/platforms/github/user-authorization-state.test.ts +++ b/apps/web/src/lib/integrations/platforms/github/user-authorization-state.test.ts @@ -1,16 +1,15 @@ import { createHash } from 'node:crypto'; -import { redisClient, redisGetDel } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { consumeGitHubUserAuthorizationState, createGitHubUserAuthorizationState, } from './user-authorization-state'; jest.mock('@/lib/redis', () => ({ - redisClient: { set: jest.fn() }, - redisGetDel: jest.fn(), + redisClient: { set: jest.fn(), getdel: jest.fn() }, })); -const mockedRedisGetDel = jest.mocked(redisGetDel); +const mockedRedisGetDel = jest.mocked(redisClient.getdel); const mockedRedisSet = jest.mocked(redisClient.set); describe('GitHub user authorization state', () => { diff --git a/apps/web/src/lib/integrations/platforms/github/user-authorization-state.ts b/apps/web/src/lib/integrations/platforms/github/user-authorization-state.ts index 7ab235951b..761fd3f659 100644 --- a/apps/web/src/lib/integrations/platforms/github/user-authorization-state.ts +++ b/apps/web/src/lib/integrations/platforms/github/user-authorization-state.ts @@ -7,7 +7,7 @@ import { OAUTH_STATE_TTL_SECONDS, verifyOAuthState, } from '@/lib/integrations/oauth-state'; -import { redisClient, redisGetDel } from '@/lib/redis'; +import { redisClient } from '@/lib/redis'; import { githubUserAuthorizationPkceRedisKey } from '@/lib/redis-keys'; const STATE_PREFIX = 'github-user-authorization:'; @@ -58,7 +58,7 @@ export async function consumeGitHubUserAuthorizationState( ); if (!parsed.success) return null; - const codeVerifier = await redisGetDel( + const codeVerifier = await redisClient.getdel( githubUserAuthorizationPkceRedisKey(parsed.data.verifierRef) ); return codeVerifier ? { codeVerifier } : null; diff --git a/apps/web/src/lib/redis.ts b/apps/web/src/lib/redis.ts index 6971c7c2c7..2bc2400df9 100644 --- a/apps/web/src/lib/redis.ts +++ b/apps/web/src/lib/redis.ts @@ -1,123 +1,11 @@ import { Redis } from '@upstash/redis'; -import { captureException } from '@sentry/nextjs'; -import type { RedisKey } from '@/lib/redis-keys'; -type RedisOperation = 'getdel' | 'del'; -type RedisConfig = { - url: string; - token: string; - source: 'upstash' | 'vercel-kv'; -}; - -// Simple GET/SET commands should still be fast over REST; anything over 200ms -// means Redis is overloaded or unreachable and callers should fail open. +// Redis commands should still be fast over REST; anything over 200ms means Redis +// is overloaded or unreachable and callers should fail open. const COMMAND_TIMEOUT_MS = 200; -let client: Redis | null = null; - export const redisClient = Redis.fromEnv({ automaticDeserialization: false, retry: false, signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS), }); - -class RedisTimeoutError extends Error { - constructor(readonly redisTimeoutMs: number) { - super('Redis timeout (command)'); - this.name = 'RedisTimeoutError'; - } -} - -function captureRedisOperationException( - err: unknown, - operation: RedisOperation, - key: RedisKey, - config: RedisConfig -) { - const timeoutPhase = err instanceof RedisTimeoutError ? 'command' : undefined; - captureException(err, { - tags: { - service: 'redis', - redis_transport: 'rest', - redis_config_source: config.source, - operation, - ...(timeoutPhase ? { redis_timeout_phase: timeoutPhase } : {}), - }, - extra: { - key, - redis_timeout_ms: err instanceof RedisTimeoutError ? err.redisTimeoutMs : undefined, - }, - }); -} - -function getRedisConfig(): RedisConfig | null { - if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) { - return { - url: process.env.UPSTASH_REDIS_REST_URL, - token: process.env.UPSTASH_REDIS_REST_TOKEN, - source: 'upstash', - }; - } - - if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { - return { - url: process.env.KV_REST_API_URL, - token: process.env.KV_REST_API_TOKEN, - source: 'vercel-kv', - }; - } - - return null; -} - -function getOrCreateClient(): { client: Redis; config: RedisConfig } | null { - const config = getRedisConfig(); - if (!config) { - return null; - } - if (!client) { - client = new Redis({ - url: config.url, - token: config.token, - automaticDeserialization: false, - retry: false, - }); - } - return { client, config }; -} - -function withTimeout(promise: Promise, ms: number): Promise { - let timer: ReturnType | undefined; - return Promise.race([ - promise.finally(() => { - if (timer) clearTimeout(timer); - }), - new Promise((_, reject) => { - timer = setTimeout(() => reject(new RedisTimeoutError(ms)), ms); - }), - ]); -} - -export async function redisGetDel(key: RedisKey): Promise { - const redis = getOrCreateClient(); - if (!redis) return null; - try { - return await withTimeout(redis.client.getdel(key), COMMAND_TIMEOUT_MS); - } catch (err) { - captureRedisOperationException(err, 'getdel', key, redis.config); - throw err; - } -} - -/** Returns false if Redis is not configured. */ -export async function redisDel(key: RedisKey): Promise { - const redis = getOrCreateClient(); - if (!redis) return false; - try { - await withTimeout(redis.client.del(key), COMMAND_TIMEOUT_MS); - return true; - } catch (err) { - captureRedisOperationException(err, 'del', key, redis.config); - throw err; - } -} From 2730a1357d058d8597567f4c35458652671de4a2 Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 4 Jun 2026 10:25:30 +0200 Subject: [PATCH 6/9] Reemove kv fallback --- apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts b/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts index 5e8dbe60f9..37f54063d4 100644 --- a/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts +++ b/apps/web/src/lib/ai-gateway/experiments/pick-variant.test.ts @@ -21,10 +21,7 @@ const upstreamB = { base_url: 'https://partner.example.com/v1', }; const redisIt = - (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) || - (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) - ? it - : it.skip; + process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN ? it : it.skip; beforeEach(async () => { await cleanupDbForTest(); From 497d7cbe8750563610a15bca23e882395402f95c Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 4 Jun 2026 11:16:53 +0200 Subject: [PATCH 7/9] ci(redis): add REST sidecar for tests --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cadb871dc..4b0beed1cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -203,12 +203,34 @@ jobs: --health-retries 5 ports: - 5432:5432 + redis: + image: redis:7 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis-http: + image: hiett/serverless-redis-http:0.0.10 + env: + SRH_MODE: env + SRH_TOKEN: example_token + SRH_CONNECTION_STRING: redis://redis:6379 + options: >- + --health-cmd "wget -qO- http://localhost:80/ >/dev/null || exit 1" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 8079:80 env: NODE_ENV: test POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/postgres POSTGRES_CONNECT_TIMEOUT: 30000 POSTGRES_MAX_QUERY_TIME: 30000 + UPSTASH_REDIS_REST_URL: http://localhost:8079 + UPSTASH_REDIS_REST_TOKEN: example_token JEST_MAX_WORKERS: 4 steps: From 3b257c29d62a809a0649dba1375a670db0fdf5bb Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 4 Jun 2026 11:20:33 +0200 Subject: [PATCH 8/9] test(redis): isolate route fetch mock --- apps/web/src/app/api/edit/completions/route.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/web/src/app/api/edit/completions/route.test.ts b/apps/web/src/app/api/edit/completions/route.test.ts index 03e5efa674..d5c8444f4c 100644 --- a/apps/web/src/app/api/edit/completions/route.test.ts +++ b/apps/web/src/app/api/edit/completions/route.test.ts @@ -16,6 +16,14 @@ jest.mock('@/lib/config.server', () => ({ jest.mock('@/lib/user/server'); jest.mock('@/lib/organizations/organization-usage'); jest.mock('@/lib/ai-gateway/byok'); +jest.mock('@/lib/redis', () => ({ + redisClient: { + get: jest.fn().mockResolvedValue(null), + set: jest.fn().mockResolvedValue('OK'), + del: jest.fn().mockResolvedValue(0), + getdel: jest.fn().mockResolvedValue(null), + }, +})); jest.mock('@/lib/debugUtils', () => ({ debugSaveProxyRequest: jest.fn(), debugSaveProxyResponseStream: jest.fn(), From 9cbf718a33398646b856f5ffe7d885a1689e801b Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Thu, 4 Jun 2026 11:33:47 +0200 Subject: [PATCH 9/9] ci(redis): fix sidecar health check --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b0beed1cc..e92a6626fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,7 +217,7 @@ jobs: SRH_TOKEN: example_token SRH_CONNECTION_STRING: redis://redis:6379 options: >- - --health-cmd "wget -qO- http://localhost:80/ >/dev/null || exit 1" + --health-cmd "wget -qO- http://127.0.0.1:80/ >/dev/null || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5