This commit is contained in:
Steve Korshakov 2025-09-08 23:35:38 -07:00
commit ae583ffaff
3 changed files with 121 additions and 0 deletions

View File

@ -18,3 +18,9 @@ S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET=happy
S3_PUBLIC_URL=http://localhost:9000/happy
# --- Voice Feature ---
# 11Labs API for voice conversations (required for voice feature)
# Secret - congiured in .env, not checked in
NODE_ENV=development

View File

@ -13,6 +13,7 @@ import { startSocket } from "./socket";
import { machinesRoutes } from "./routes/machinesRoutes";
import { devRoutes } from "./routes/devRoutes";
import { versionRoutes } from "./routes/versionRoutes";
import { voiceRoutes } from "./routes/voiceRoutes";
import { enableMonitoring } from "./utils/enableMonitoring";
import { enableErrorHandlers } from "./utils/enableErrorHandlers";
import { enableAuthentication } from "./utils/enableAuthentication";
@ -55,6 +56,7 @@ export async function startApi(eventRouter: EventRouter) {
machinesRoutes(typed, eventRouter);
devRoutes(typed);
versionRoutes(typed);
voiceRoutes(typed);
// Start HTTP
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3005;

View File

@ -0,0 +1,113 @@
import { z } from "zod";
import { type Fastify } from "../types";
import { log } from "@/utils/log";
export function voiceRoutes(app: Fastify) {
app.post('/v1/voice/token', {
preHandler: app.authenticate,
schema: {
body: z.object({
agentId: z.string(),
revenueCatPublicKey: z.string().optional()
}),
response: {
200: z.object({
allowed: z.boolean(),
token: z.string().optional(),
agentId: z.string().optional()
}),
400: z.object({
allowed: z.boolean(),
error: z.string()
})
}
}
}, async (request, reply) => {
const userId = request.userId; // CUID from JWT
const { agentId, revenueCatPublicKey } = request.body;
log({ module: 'voice' }, `Voice token request from user ${userId}`);
const isDevelopment = process.env.NODE_ENV === 'development' || process.env.ENV === 'dev';
// Production requires RevenueCat key
if (!isDevelopment && !revenueCatPublicKey) {
log({ module: 'voice' }, 'Production environment requires RevenueCat public key');
return reply.code(400).send({
allowed: false,
error: 'RevenueCat public key required'
});
}
// Check subscription in production
if (!isDevelopment && revenueCatPublicKey) {
const response = await fetch(
`https://api.revenuecat.com/v1/subscribers/${userId}`,
{
method: 'GET',
headers: {
'Authorization': `Bearer ${revenueCatPublicKey}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
log({ module: 'voice' }, `RevenueCat check failed for user ${userId}: ${response.status}`);
return reply.send({
allowed: false,
agentId
});
}
const data = await response.json() as any;
const proEntitlement = data.subscriber?.entitlements?.active?.pro;
if (!proEntitlement) {
log({ module: 'voice' }, `User ${userId} does not have active subscription`);
return reply.send({
allowed: false,
agentId
});
}
}
// Check if 11Labs API key is configured
const elevenLabsApiKey = process.env.ELEVENLABS_API_KEY;
if (!elevenLabsApiKey) {
log({ module: 'voice' }, 'Missing 11Labs API key');
return reply.code(400).send({ allowed: false, error: 'Missing 11Labs API key on the server' });
}
// Get 11Labs conversation token
const response = await fetch(
`https://api.elevenlabs.io/v1/convai/conversation/token?agent_id=${agentId}`,
{
method: 'GET',
headers: {
'xi-api-key': elevenLabsApiKey,
'Accept': 'application/json'
}
}
);
if (!response.ok) {
log({ module: 'voice' }, `Failed to get 11Labs token for user ${userId}`);
return reply.code(400).send({
allowed: false,
error: `Failed to get 11Labs token for user ${userId}`
});
}
const data = await response.json() as any;
console.log(data);
const token = data.token;
log({ module: 'voice' }, `Voice token issued for user ${userId}`);
return reply.send({
allowed: true,
token,
agentId
});
});
}