feat(server): enhance config guard with custom error messages and add FLUX_PACKAGES handling
This commit is contained in:
@@ -9,12 +9,16 @@ 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> {
|
||||
export function configGuard(
|
||||
configKV: ConfigKVService,
|
||||
keys: Parameters<ConfigKVService['get']>[0][],
|
||||
message = 'Service is not available yet',
|
||||
): 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')
|
||||
throw createServiceUnavailableError(message, 'CONFIG_NOT_SET')
|
||||
}
|
||||
await next()
|
||||
}
|
||||
|
||||
@@ -20,9 +20,13 @@ 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'])
|
||||
const fluxConfigGuard = configGuard(configKV, ['FLUX_PER_CENT'], 'Top-up is not available yet')
|
||||
|
||||
return new Hono<HonoEnv>()
|
||||
.get('/packages', async (c) => {
|
||||
const packages = await configKV.getOptional('FLUX_PACKAGES')
|
||||
return c.json(packages ?? [])
|
||||
})
|
||||
.post('/checkout', authGuard, fluxConfigGuard, async (c) => {
|
||||
if (!stripe)
|
||||
throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED')
|
||||
|
||||
@@ -54,7 +54,7 @@ export function createV1CompletionsRoutes(fluxService: FluxService, configKV: Co
|
||||
}
|
||||
|
||||
return new Hono<HonoEnv>()
|
||||
.use('*', authGuard, configGuard(configKV, ['FLUX_PER_REQUEST']))
|
||||
.use('*', authGuard, configGuard(configKV, ['FLUX_PER_REQUEST'], 'Service is not available yet'))
|
||||
.post('/chat/completions', handleCompletion)
|
||||
.post('/chat/completion', handleCompletion)
|
||||
}
|
||||
|
||||
@@ -71,4 +71,42 @@ describe('configKVService', () => {
|
||||
const value = await service.get('INITIAL_USER_FLUX')
|
||||
expect(value).toBe(500)
|
||||
})
|
||||
|
||||
// --- FLUX_PACKAGES (JSON) ---
|
||||
|
||||
it('get FLUX_PACKAGES should parse JSON array', async () => {
|
||||
const packages = [
|
||||
{ amount: 500, label: '500 Flux', price: '$5' },
|
||||
{ amount: 1000, label: '1000 Flux', price: '$10' },
|
||||
]
|
||||
redis._store.set('config:FLUX_PACKAGES', JSON.stringify(packages))
|
||||
|
||||
const value = await service.get('FLUX_PACKAGES')
|
||||
expect(value).toEqual(packages)
|
||||
})
|
||||
|
||||
it('set FLUX_PACKAGES should serialize as JSON', async () => {
|
||||
const packages = [{ amount: 500, label: '500 Flux', price: '$5' }]
|
||||
await service.set('FLUX_PACKAGES', packages)
|
||||
|
||||
const stored = redis._store.get('config:FLUX_PACKAGES')
|
||||
expect(stored).toBe(JSON.stringify(packages))
|
||||
})
|
||||
|
||||
it('fLUX_PACKAGES round-trip should preserve structure', async () => {
|
||||
const packages = [
|
||||
{ amount: 500, label: '500 Flux', price: '$5' },
|
||||
{ amount: 1000, label: '1000 Flux', price: '$10' },
|
||||
{ amount: 5000, label: '5000 Flux', price: '$45' },
|
||||
]
|
||||
await service.set('FLUX_PACKAGES', packages)
|
||||
|
||||
const value = await service.get('FLUX_PACKAGES')
|
||||
expect(value).toEqual(packages)
|
||||
})
|
||||
|
||||
it('getOptional FLUX_PACKAGES should return null when not set', async () => {
|
||||
const value = await service.getOptional('FLUX_PACKAGES')
|
||||
expect(value).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,14 +2,36 @@ import type Redis from 'ioredis'
|
||||
|
||||
import { createServiceUnavailableError } from '../utils/error'
|
||||
|
||||
export interface FluxPackage {
|
||||
/** Amount in cents sent to Stripe */
|
||||
amount: number
|
||||
/** Display label, e.g. "500 Flux" */
|
||||
label: string
|
||||
/** Display price, e.g. "$5" */
|
||||
price: string
|
||||
}
|
||||
|
||||
interface ConfigDefinitions {
|
||||
FLUX_PER_CENT: number
|
||||
FLUX_PER_REQUEST: number
|
||||
INITIAL_USER_FLUX: number
|
||||
FLUX_PACKAGES: FluxPackage[]
|
||||
}
|
||||
|
||||
const KEY_PREFIX = 'config:'
|
||||
|
||||
function parseValue<K extends keyof ConfigDefinitions>(key: K, raw: string): ConfigDefinitions[K] {
|
||||
if (key === 'FLUX_PACKAGES')
|
||||
return JSON.parse(raw) as ConfigDefinitions[K]
|
||||
return Number(raw) as ConfigDefinitions[K]
|
||||
}
|
||||
|
||||
function serializeValue<K extends keyof ConfigDefinitions>(key: K, value: ConfigDefinitions[K]): string {
|
||||
if (key === 'FLUX_PACKAGES')
|
||||
return JSON.stringify(value)
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export function createConfigKVService(redis: Redis) {
|
||||
return {
|
||||
async getOptional<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K] | null> {
|
||||
@@ -17,7 +39,7 @@ export function createConfigKVService(redis: Redis) {
|
||||
if (raw === null)
|
||||
return null
|
||||
|
||||
return Number(raw) as ConfigDefinitions[K]
|
||||
return parseValue(key, raw)
|
||||
},
|
||||
|
||||
async get<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K]> {
|
||||
@@ -29,7 +51,7 @@ export function createConfigKVService(redis: Redis) {
|
||||
},
|
||||
|
||||
async set<K extends keyof ConfigDefinitions>(key: K, value: ConfigDefinitions[K]): Promise<void> {
|
||||
await redis.set(`${KEY_PREFIX}${key}`, String(value))
|
||||
await redis.set(`${KEY_PREFIX}${key}`, serializeValue(key, value))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user