From b2d74429498f3443f372e54f075806b77ba725bc Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Wed, 1 Jul 2026 21:57:46 +0800 Subject: [PATCH] feat(server): enforce official asr aliases --- apps/server/src/app.ts | 1 + .../audio-transcription-stream/route.test.ts | 107 +++++++++++++++++- .../audio-transcription-stream/route.ts | 19 +++- 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index f2dd40335..492c1c4b3 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -230,6 +230,7 @@ export async function buildApp(deps: AppDeps) { env: deps.env, configKV: deps.configKV, envelopeCrypto: deps.envelopeCrypto, + officialCatalogService: deps.officialCatalogService, })) // Cross-instance config invalidation. The subscriber owns its own diff --git a/apps/server/src/routes/audio-transcription-stream/route.test.ts b/apps/server/src/routes/audio-transcription-stream/route.test.ts index 64275a05b..b6621d926 100644 --- a/apps/server/src/routes/audio-transcription-stream/route.test.ts +++ b/apps/server/src/routes/audio-transcription-stream/route.test.ts @@ -1,11 +1,13 @@ import type { RouterConfig } from '../../services/domain/llm-router/types' +import type { OfficialCatalogService } from '../../services/domain/official-catalog' import { Buffer } from 'node:buffer' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { createEnvelopeCrypto } from '../../utils/envelope-crypto' -import { resolveOfficialAliyunNlsCredentials } from './route' +import { ApiError } from '../../utils/error' +import { resolveOfficialAliyunNlsCredentials, resolveOfficialAliyunNlsCredentialsFromConfig } from './route' function createRouterConfig(overrides?: Partial): RouterConfig { return { @@ -20,6 +22,35 @@ function createRouterConfig(overrides?: Partial): RouterConfig { } } +function createOfficialCatalogService(routeModelId = 'auto'): OfficialCatalogService { + return { + syncAliasesFromRouterConfig: vi.fn(async () => []), + resolveEnabledAlias: vi.fn(async () => ({ + id: 'alias-auto', + surface: 'asr', + aliasId: 'auto', + displayName: 'Auto', + enabled: true, + displayOrder: 0, + fallbackEnabled: true, + loadBalancingEnabled: false, + createdAt: new Date(), + updatedAt: new Date(), + routes: [{ + id: 'route-1', + aliasId: 'alias-auto', + routerModelId: routeModelId, + pool: 'primary', + enabled: true, + weight: 1, + displayOrder: 0, + createdAt: new Date(), + updatedAt: new Date(), + }], + })), + } as unknown as OfficialCatalogService +} + describe('resolveOfficialAliyunNlsCredentials', () => { /** * @example @@ -69,4 +100,76 @@ describe('resolveOfficialAliyunNlsCredentials', () => { region: 'cn-shanghai', }) }) + + it('resolves official ASR alias through the catalog before decrypting credentials', async () => { + const envelope = createEnvelopeCrypto({ masterKey: Buffer.alloc(32, 7) }) + const ciphertext = envelope.encryptKey(' secret ', { + modelName: 'aliyun/asr-primary', + keyEntryId: 'aliyun-nls-asr-prod-1', + }) + const routerConfig = createRouterConfig({ + asr: { + models: { + 'aliyun/asr-primary': { + provider: 'aliyun-nls', + upstreams: [{ + keys: [{ id: 'aliyun-nls-asr-prod-1', ciphertext }], + adapterParams: { + accessKeyId: 'ak', + appKey: 'app', + }, + }], + }, + }, + }, + }) + const officialCatalogService = createOfficialCatalogService('aliyun/asr-primary') + + const credentials = await resolveOfficialAliyunNlsCredentialsFromConfig({ + configKV: { getOptional: vi.fn(async () => routerConfig) } as never, + envelopeCrypto: envelope, + officialCatalogService, + }) + + expect(credentials).toMatchObject({ + accessKeyId: 'ak', + accessKeySecret: 'secret', + appKey: 'app', + }) + expect(officialCatalogService.syncAliasesFromRouterConfig).toHaveBeenCalledWith({ + surface: 'asr', + modelIds: ['aliyun/asr-primary'], + }) + expect(officialCatalogService.resolveEnabledAlias).toHaveBeenCalledWith('asr', 'auto') + }) + + it('rejects disabled official ASR aliases before credentials are used', async () => { + const envelope = createEnvelopeCrypto({ masterKey: Buffer.alloc(32, 7) }) + const officialCatalogService = createOfficialCatalogService() + vi.mocked(officialCatalogService.resolveEnabledAlias).mockRejectedValueOnce( + new ApiError(400, 'OFFICIAL_ALIAS_DISABLED', 'Official provider alias is disabled'), + ) + const routerConfig = createRouterConfig({ + asr: { + models: { + auto: { + provider: 'aliyun-nls', + upstreams: [{ + keys: [{ id: 'aliyun-nls-asr-prod-1', ciphertext: 'unused' }], + adapterParams: {}, + }], + }, + }, + }, + }) + + await expect(resolveOfficialAliyunNlsCredentialsFromConfig({ + configKV: { getOptional: vi.fn(async () => routerConfig) } as never, + envelopeCrypto: envelope, + officialCatalogService, + })).rejects.toMatchObject({ + statusCode: 400, + errorCode: 'OFFICIAL_ALIAS_DISABLED', + }) + }) }) diff --git a/apps/server/src/routes/audio-transcription-stream/route.ts b/apps/server/src/routes/audio-transcription-stream/route.ts index a693ed2f7..46154128e 100644 --- a/apps/server/src/routes/audio-transcription-stream/route.ts +++ b/apps/server/src/routes/audio-transcription-stream/route.ts @@ -4,6 +4,7 @@ import type { AuthInstance } from '../../libs/auth' import type { Env } from '../../libs/env' import type { ConfigKVService } from '../../services/adapters/config-kv' import type { RouterConfig } from '../../services/domain/llm-router/types' +import type { OfficialCatalogService } from '../../services/domain/official-catalog' import type { EnvelopeCrypto } from '../../utils/envelope-crypto' import { resolveRequestAuth } from '../../libs/request-auth' @@ -84,12 +85,25 @@ export function resolveOfficialAliyunNlsCredentials( } } -async function resolveOfficialAliyunNlsCredentialsFromConfig(input: { +export async function resolveOfficialAliyunNlsCredentialsFromConfig(input: { configKV: ConfigKVService envelopeCrypto: EnvelopeCrypto + officialCatalogService: OfficialCatalogService }) { const routerConfig = await input.configKV.getOptional('LLM_ROUTER_CONFIG') - const credentials = resolveOfficialAliyunNlsCredentials(routerConfig, input.envelopeCrypto) + const modelIds = Object.keys(routerConfig?.asr?.models ?? {}).sort() + if (modelIds.length === 0) + return null + + await input.officialCatalogService.syncAliasesFromRouterConfig({ + surface: 'asr', + modelIds, + }) + + const alias = await input.officialCatalogService.resolveEnabledAlias('asr', OFFICIAL_ASR_MODEL_NAME) + const primary = alias.routes.find(route => route.pool === 'primary') + const modelName = (primary ?? alias.routes[0]).routerModelId + const credentials = resolveOfficialAliyunNlsCredentials(routerConfig, input.envelopeCrypto, modelName) if (!credentials) return null @@ -113,6 +127,7 @@ export function createAudioTranscriptionStreamHandler(input: { env: Env configKV: ConfigKVService envelopeCrypto: EnvelopeCrypto + officialCatalogService: OfficialCatalogService }) { return async function handleAudioTranscriptionStream(c: Context) { const session = await resolveRequestAuth(