- Add POST /v1/voice/token endpoint for 11Labs voice authentication - Integrate RevenueCat subscription validation (pro entitlement required) - Skip paywall in development (NODE_ENV=development) - Fetch 11Labs conversation tokens for authorized users - Return allowed:false for users without subscription - Add NODE_ENV=development to .env.dev for local testing Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
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
|
|
});
|
|
});
|
|
} |