-
Notifications
You must be signed in to change notification settings - Fork 35
added 60db voice services #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { NextResponse } from 'next/server'; | ||
| import { auth } from '@clerk/nextjs/server'; | ||
|
|
||
| /** | ||
| * Server-side proxy for 60db discovery endpoints. Keeps the API key off | ||
| * the wire so the browser can populate the voice / model pickers safely. | ||
| * | ||
| * GET /api/sixtydb/discovery?kind=default-voices -> /default-voices | ||
| * GET /api/sixtydb/discovery?kind=my-voices -> /my-voices | ||
| * GET /api/sixtydb/discovery?kind=tts-models -> /tts/models | ||
| * GET /api/sixtydb/discovery?kind=stt-models -> /stt/models | ||
| */ | ||
| const PATH_MAP: Record<string, string> = { | ||
| 'default-voices': '/default-voices', | ||
| 'my-voices': '/my-voices', | ||
| 'tts-models': '/tts/models', | ||
| 'stt-models': '/stt/models', | ||
| }; | ||
|
|
||
| export async function GET(request: Request) { | ||
| const { userId } = await auth(); | ||
| if (!userId) { | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); | ||
| } | ||
|
|
||
| const apiKey = process.env.SIXTYDB_API_KEY; | ||
| if (!apiKey) { | ||
| return NextResponse.json( | ||
| { error: 'SIXTYDB_API_KEY not set on the server.' }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
|
|
||
| const kind = new URL(request.url).searchParams.get('kind') ?? ''; | ||
| const path = PATH_MAP[kind]; | ||
| if (!path) { | ||
| return NextResponse.json( | ||
| { | ||
| error: `Unknown kind '${kind}'. Use one of: ${Object.keys(PATH_MAP).join(', ')}.`, | ||
| }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| const apiBase = (process.env.SIXTYDB_API_BASE || 'https://api.60db.ai').replace(/\/$/, ''); | ||
|
|
||
| try { | ||
| const res = await fetch(`${apiBase}${path}`, { | ||
| headers: { Authorization: `Bearer ${apiKey}` }, | ||
| cache: 'no-store', | ||
| }); | ||
| const body = await res.text(); | ||
| return new NextResponse(body, { | ||
| status: res.status, | ||
| headers: { 'Content-Type': res.headers.get('Content-Type') ?? 'application/json' }, | ||
| }); | ||
| } catch (err) { | ||
| return NextResponse.json( | ||
| { error: `60db ${path} request failed: ${(err as Error).message}` }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { NextResponse } from 'next/server'; | ||
| import { auth } from '@clerk/nextjs/server'; | ||
|
|
||
| /** | ||
| * Returns the 60db API key to the browser so it can open the STT/TTS | ||
| * WebSockets. 60db has no token-mint endpoint yet, so this exposes the | ||
| * long-lived key. Gated behind Clerk auth so only signed-in users hit it. | ||
| */ | ||
| export async function POST() { | ||
| const { userId } = await auth(); | ||
| if (!userId) { | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); | ||
| } | ||
|
|
||
| const apiKey = process.env.SIXTYDB_API_KEY; | ||
| if (!apiKey) { | ||
| return NextResponse.json( | ||
| { error: 'SIXTYDB_API_KEY not set on the server.' }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
|
|
||
| return NextResponse.json({ | ||
| api_key: apiKey, | ||
| api_base: process.env.SIXTYDB_API_BASE || 'https://api.60db.ai', | ||
| }); | ||
|
Comment on lines
+23
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not return the long-lived At Line 24, this endpoint returns a reusable server secret to clients. Any signed-in user can exfiltrate and use it off-platform, bypassing your app’s controls and potentially incurring uncontrolled spend. Use a server-side WebSocket proxy (or provider-issued short-lived token when available) so the long-lived key never leaves server trust boundaries. 🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add an explicit timeout to the upstream discovery fetch.
At Line 48,
fetchhas no timeout. If 60db stalls, this route can hang until platform-level timeout, tying up request capacity.Suggested patch
export async function GET(request: Request) { @@ - try { - const res = await fetch(`${apiBase}${path}`, { - headers: { Authorization: `Bearer ${apiKey}` }, - cache: 'no-store', - }); - const body = await res.text(); - return new NextResponse(body, { - status: res.status, - headers: { 'Content-Type': res.headers.get('Content-Type') ?? 'application/json' }, - }); - } catch (err) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 8000); + try { + const res = await fetch(`${apiBase}${path}`, { + headers: { Authorization: `Bearer ${apiKey}` }, + cache: 'no-store', + signal: controller.signal, + }); + const body = await res.text(); + return new NextResponse(body, { + status: res.status, + headers: { 'Content-Type': res.headers.get('Content-Type') ?? 'application/json' }, + }); + } finally { + clearTimeout(timeout); + } + } catch (err) { + if ((err as Error).name === 'AbortError') { + return NextResponse.json( + { error: `60db ${path} request timed out.` }, + { status: 504 }, + ); + } return NextResponse.json( { error: `60db ${path} request failed: ${(err as Error).message}` }, { status: 502 }, ); } }🤖 Prompt for AI Agents