import { NextRequest } from 'next/server'; import { createLogger } from '@/lib/logger'; import { apiError, apiSuccess } from '@/lib/server/api-response'; import { fetchPinnedPublicUrl, PublicUrlFetchError } from '@/lib/server/public-url-fetch'; const log = createLogger('Azure Voices'); export const maxDuration = 30; export const runtime = 'nodejs'; function publicFetchError(error: unknown) { if ( error instanceof PublicUrlFetchError && (error.code === 'INVALID_URL' || error.code === 'BLOCKED_TARGET' || error.code === 'DNS_FAILURE') ) { return apiError('INVALID_URL', 403, 'Azure endpoint is not an allowed public URL'); } return apiError('UPSTREAM_ERROR', 502, 'Unable to connect to Azure Speech Services'); } /** * Azure TTS Voice List API * Fetches available voices from Azure Speech Services */ export async function POST(req: NextRequest) { let baseUrl: string | undefined; try { const body = await req.json(); const { apiKey } = body; baseUrl = body.baseUrl; if (typeof apiKey !== 'string' || !apiKey) { return apiError('MISSING_API_KEY', 400, 'API Key is required'); } if (typeof baseUrl !== 'string' || !baseUrl) { return apiError('MISSING_REQUIRED_FIELD', 400, 'Base URL is required'); } let voicesUrl: string; try { voicesUrl = new URL( 'cognitiveservices/voices/list', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`, ).href; } catch { return apiError('INVALID_URL', 403, 'Azure endpoint is not an allowed public URL'); } let pinnedResponse; try { pinnedResponse = await fetchPinnedPublicUrl(voicesUrl, { headers: { 'Ocp-Apim-Subscription-Key': apiKey }, signal: AbortSignal.timeout(25_000), maxResponseBytes: 8 * 1024 * 1024, }); } catch (error) { return publicFetchError(error); } const { response } = pinnedResponse; try { if (response.status >= 300 && response.status < 400) { return apiError('REDIRECT_NOT_ALLOWED', 403, 'Redirects are not allowed'); } if (!response.ok) { const errorText = await response.text(); return apiError( 'UPSTREAM_ERROR', response.status, 'Failed to fetch voices from Azure', errorText || response.statusText, ); } const voices = await response.json(); return apiSuccess({ voices }); } finally { await pinnedResponse.dispose(); } } catch (error) { log.error(`Azure voices fetch failed [baseUrl="${baseUrl ?? 'unknown'}"]:`, error); return apiError('INTERNAL_ERROR', 500, 'Failed to fetch voices'); } }