fix(server): resolve config kv properly
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import type { MiddlewareHandler } from 'hono'
|
||||
|
||||
import type { ConfigKVService } from '../services/config-kv'
|
||||
import type { HonoEnv } from '../types/hono'
|
||||
|
||||
import { createServiceUnavailableError } from '../utils/error'
|
||||
|
||||
/**
|
||||
* Middleware factory that checks required config keys exist in Redis.
|
||||
* Returns 503 if any key is missing.
|
||||
*/
|
||||
export function configGuard(configKV: ConfigKVService, keys: Parameters<ConfigKVService['get']>[0][]): MiddlewareHandler<HonoEnv> {
|
||||
return async (_c, next) => {
|
||||
for (const key of keys) {
|
||||
const value = await configKV.getOptional(key)
|
||||
if (value === null)
|
||||
throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET')
|
||||
}
|
||||
await next()
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { Hono } from 'hono'
|
||||
import { integer, minValue, number, object, pipe, safeParse } from 'valibot'
|
||||
|
||||
import { authGuard } from '../middlewares/auth'
|
||||
import { configGuard } from '../middlewares/config-guard'
|
||||
import { createBadRequestError, createServiceUnavailableError } from '../utils/error'
|
||||
|
||||
const CheckoutBodySchema = object({
|
||||
@@ -19,8 +20,10 @@ const CheckoutBodySchema = object({
|
||||
export function createStripeRoutes(fluxService: FluxService, stripeService: StripeService, configKV: ConfigKVService, env: Env) {
|
||||
const stripe = env.STRIPE_SECRET_KEY ? new Stripe(env.STRIPE_SECRET_KEY) : null
|
||||
|
||||
const fluxConfigGuard = configGuard(configKV, ['FLUX_PER_CENT'])
|
||||
|
||||
return new Hono<HonoEnv>()
|
||||
.post('/checkout', authGuard, async (c) => {
|
||||
.post('/checkout', authGuard, fluxConfigGuard, async (c) => {
|
||||
if (!stripe)
|
||||
throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED')
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { HonoEnv } from '../types/hono'
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { authGuard } from '../middlewares/auth'
|
||||
import { configGuard } from '../middlewares/config-guard'
|
||||
import { createPaymentRequiredError } from '../utils/error'
|
||||
|
||||
// Only forward these headers from the upstream LLM response
|
||||
@@ -53,7 +54,7 @@ export function createV1CompletionsRoutes(fluxService: FluxService, configKV: Co
|
||||
}
|
||||
|
||||
return new Hono<HonoEnv>()
|
||||
.use('*', authGuard)
|
||||
.use('*', authGuard, configGuard(configKV, ['FLUX_PER_REQUEST']))
|
||||
.post('/chat/completions', handleCompletion)
|
||||
.post('/chat/completion', handleCompletion)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createConfigKVService } from '../config-kv'
|
||||
|
||||
function createMockRedis() {
|
||||
const store = new Map<string, string>()
|
||||
return {
|
||||
get: vi.fn(async (key: string) => store.get(key) ?? null),
|
||||
set: vi.fn(async (key: string, value: string) => { store.set(key, value) }),
|
||||
_store: store,
|
||||
}
|
||||
}
|
||||
|
||||
describe('configKVService', () => {
|
||||
let redis: ReturnType<typeof createMockRedis>
|
||||
let service: ReturnType<typeof createConfigKVService>
|
||||
|
||||
beforeEach(() => {
|
||||
redis = createMockRedis()
|
||||
service = createConfigKVService(redis as any)
|
||||
})
|
||||
|
||||
// --- get ---
|
||||
|
||||
it('get should throw 503 when key is not set', async () => {
|
||||
await expect(service.get('FLUX_PER_CENT'))
|
||||
.rejects
|
||||
.toThrow('Config key "FLUX_PER_CENT" is not set in Redis')
|
||||
})
|
||||
|
||||
it('get should return numeric value when key is set', async () => {
|
||||
redis._store.set('config:FLUX_PER_CENT', '5')
|
||||
|
||||
const value = await service.get('FLUX_PER_CENT')
|
||||
expect(value).toBe(5)
|
||||
})
|
||||
|
||||
it('get should read from correct prefixed key', async () => {
|
||||
redis._store.set('config:FLUX_PER_REQUEST', '3')
|
||||
|
||||
await service.get('FLUX_PER_REQUEST')
|
||||
expect(redis.get).toHaveBeenCalledWith('config:FLUX_PER_REQUEST')
|
||||
})
|
||||
|
||||
// --- getOptional ---
|
||||
|
||||
it('getOptional should return null when key is not set', async () => {
|
||||
const value = await service.getOptional('FLUX_PER_CENT')
|
||||
expect(value).toBeNull()
|
||||
})
|
||||
|
||||
it('getOptional should return numeric value when key is set', async () => {
|
||||
redis._store.set('config:INITIAL_USER_FLUX', '200')
|
||||
|
||||
const value = await service.getOptional('INITIAL_USER_FLUX')
|
||||
expect(value).toBe(200)
|
||||
})
|
||||
|
||||
// --- set ---
|
||||
|
||||
it('set should write value to Redis with prefix', async () => {
|
||||
await service.set('FLUX_PER_CENT', 10)
|
||||
|
||||
expect(redis.set).toHaveBeenCalledWith('config:FLUX_PER_CENT', '10')
|
||||
expect(redis._store.get('config:FLUX_PER_CENT')).toBe('10')
|
||||
})
|
||||
|
||||
it('set then get should round-trip correctly', async () => {
|
||||
await service.set('INITIAL_USER_FLUX', 500)
|
||||
|
||||
const value = await service.get('INITIAL_USER_FLUX')
|
||||
expect(value).toBe(500)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,21 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import type { createConfigKVService } from '../config-kv'
|
||||
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { mockDB } from '../../libs/mock-db'
|
||||
import { createFluxService } from '../flux'
|
||||
|
||||
import * as schema from '../../schemas'
|
||||
|
||||
function createMockConfigKV(overrides: Record<string, number> = {}): ReturnType<typeof createConfigKVService> {
|
||||
const defaults: Record<string, number> = { INITIAL_USER_FLUX: 100, FLUX_PER_CENT: 1, FLUX_PER_REQUEST: 1, ...overrides }
|
||||
return {
|
||||
get: vi.fn(async (key: string) => defaults[key]),
|
||||
getOptional: vi.fn(async (key: string) => defaults[key] ?? null),
|
||||
set: vi.fn(),
|
||||
} as any
|
||||
}
|
||||
|
||||
describe('fluxService', () => {
|
||||
let db: any
|
||||
let service: ReturnType<typeof createFluxService>
|
||||
@@ -12,7 +23,7 @@ describe('fluxService', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await mockDB(schema)
|
||||
service = createFluxService(db)
|
||||
service = createFluxService(db, createMockConfigKV())
|
||||
|
||||
// Create a test user for foreign key constraints
|
||||
const [user] = await db.insert(schema.user).values({
|
||||
|
||||
@@ -12,14 +12,22 @@ const KEY_PREFIX = 'config:'
|
||||
|
||||
export function createConfigKVService(redis: Redis) {
|
||||
return {
|
||||
async get<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K]> {
|
||||
async getOptional<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K] | null> {
|
||||
const raw = await redis.get(`${KEY_PREFIX}${key}`)
|
||||
if (raw === null)
|
||||
throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET')
|
||||
return null
|
||||
|
||||
return Number(raw) as ConfigDefinitions[K]
|
||||
},
|
||||
|
||||
async get<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K]> {
|
||||
const value = await this.getOptional(key)
|
||||
if (value === null)
|
||||
throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET')
|
||||
|
||||
return value
|
||||
},
|
||||
|
||||
async set<K extends keyof ConfigDefinitions>(key: K, value: ConfigDefinitions[K]): Promise<void> {
|
||||
await redis.set(`${KEY_PREFIX}${key}`, String(value))
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user