Skip to content

Commit 73940ab

Browse files
fix(deployed-chat): voice mode (#2358)
* fix(deployed-chat): voice mode * remove redundant check * consolidate query * invalidate session on password change + race condition fix
1 parent f111dac commit 73940ab

File tree

8 files changed

+136
-91
lines changed

8 files changed

+136
-91
lines changed

apps/sim/app/(landing)/actions/github.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createLogger } from '@/lib/logs/console/logger'
22

3-
const DEFAULT_STARS = '18.6k'
3+
const DEFAULT_STARS = '19.4k'
44

55
const logger = createLogger('GitHubStars')
66

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export async function POST(
132132
if ((password || email) && !input) {
133133
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
134134

135-
setChatAuthCookie(response, deployment.id, deployment.authType)
135+
setChatAuthCookie(response, deployment.id, deployment.authType, deployment.password)
136136

137137
return response
138138
}
@@ -315,7 +315,7 @@ export async function GET(
315315
if (
316316
deployment.authType !== 'public' &&
317317
authCookie &&
318-
validateAuthToken(authCookie.value, deployment.id)
318+
validateAuthToken(authCookie.value, deployment.id, deployment.password)
319319
) {
320320
return addCorsHeaders(
321321
createSuccessResponse({

apps/sim/app/api/chat/utils.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHash } from 'crypto'
12
import { db } from '@sim/db'
23
import { chat, workflow } from '@sim/db/schema'
34
import { eq } from 'drizzle-orm'
@@ -9,6 +10,10 @@ import { hasAdminPermission } from '@/lib/workspaces/permissions/utils'
910

1011
const logger = createLogger('ChatAuthUtils')
1112

13+
function hashPassword(encryptedPassword: string): string {
14+
return createHash('sha256').update(encryptedPassword).digest('hex').substring(0, 8)
15+
}
16+
1217
/**
1318
* Check if user has permission to create a chat for a specific workflow
1419
* Either the user owns the workflow directly OR has admin permission for the workflow's workspace
@@ -77,43 +82,61 @@ export async function checkChatAccess(
7782
return { hasAccess: false }
7883
}
7984

80-
const encryptAuthToken = (chatId: string, type: string): string => {
81-
return Buffer.from(`${chatId}:${type}:${Date.now()}`).toString('base64')
85+
function encryptAuthToken(chatId: string, type: string, encryptedPassword?: string | null): string {
86+
const pwHash = encryptedPassword ? hashPassword(encryptedPassword) : ''
87+
return Buffer.from(`${chatId}:${type}:${Date.now()}:${pwHash}`).toString('base64')
8288
}
8389

84-
export const validateAuthToken = (token: string, chatId: string): boolean => {
90+
export function validateAuthToken(
91+
token: string,
92+
chatId: string,
93+
encryptedPassword?: string | null
94+
): boolean {
8595
try {
8696
const decoded = Buffer.from(token, 'base64').toString()
87-
const [storedId, _type, timestamp] = decoded.split(':')
97+
const parts = decoded.split(':')
98+
const [storedId, _type, timestamp, storedPwHash] = parts
8899

89100
if (storedId !== chatId) {
90101
return false
91102
}
92103

93104
const createdAt = Number.parseInt(timestamp)
94105
const now = Date.now()
95-
const expireTime = 24 * 60 * 60 * 1000 // 24 hours
106+
const expireTime = 24 * 60 * 60 * 1000
96107

97108
if (now - createdAt > expireTime) {
98109
return false
99110
}
100111

112+
if (encryptedPassword) {
113+
const currentPwHash = hashPassword(encryptedPassword)
114+
if (storedPwHash !== currentPwHash) {
115+
return false
116+
}
117+
}
118+
101119
return true
102120
} catch (_e) {
103121
return false
104122
}
105123
}
106124

107-
export const setChatAuthCookie = (response: NextResponse, chatId: string, type: string): void => {
108-
const token = encryptAuthToken(chatId, type)
125+
export function setChatAuthCookie(
126+
response: NextResponse,
127+
chatId: string,
128+
type: string,
129+
encryptedPassword?: string | null
130+
): void {
131+
const token = encryptAuthToken(chatId, type, encryptedPassword)
109132
response.cookies.set({
110133
name: `chat_auth_${chatId}`,
111134
value: token,
112135
httpOnly: true,
113136
secure: !isDev,
114137
sameSite: 'lax',
115138
path: '/',
116-
maxAge: 60 * 60 * 24, // 24 hours
139+
maxAge: 60 * 60 * 24,
117140
})
118141
}
119142

@@ -145,7 +168,7 @@ export async function validateChatAuth(
145168
const cookieName = `chat_auth_${deployment.id}`
146169
const authCookie = request.cookies.get(cookieName)
147170

148-
if (authCookie && validateAuthToken(authCookie.value, deployment.id)) {
171+
if (authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password)) {
149172
return { authorized: true }
150173
}
151174

apps/sim/app/api/proxy/tts/stream/route.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,81 @@
1+
import { db } from '@sim/db'
2+
import { chat } from '@sim/db/schema'
3+
import { eq } from 'drizzle-orm'
14
import type { NextRequest } from 'next/server'
2-
import { checkHybridAuth } from '@/lib/auth/hybrid'
35
import { env } from '@/lib/core/config/env'
46
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
57
import { createLogger } from '@/lib/logs/console/logger'
8+
import { validateAuthToken } from '@/app/api/chat/utils'
69

710
const logger = createLogger('ProxyTTSStreamAPI')
811

12+
/**
13+
* Validates chat-based authentication for deployed chat voice mode
14+
* Checks if the user has a valid chat auth cookie for the given chatId
15+
*/
16+
async function validateChatAuth(request: NextRequest, chatId: string): Promise<boolean> {
17+
try {
18+
const chatResult = await db
19+
.select({
20+
id: chat.id,
21+
isActive: chat.isActive,
22+
authType: chat.authType,
23+
password: chat.password,
24+
})
25+
.from(chat)
26+
.where(eq(chat.id, chatId))
27+
.limit(1)
28+
29+
if (chatResult.length === 0 || !chatResult[0].isActive) {
30+
logger.warn('Chat not found or inactive for TTS auth:', chatId)
31+
return false
32+
}
33+
34+
const chatData = chatResult[0]
35+
36+
if (chatData.authType === 'public') {
37+
return true
38+
}
39+
40+
const cookieName = `chat_auth_${chatId}`
41+
const authCookie = request.cookies.get(cookieName)
42+
43+
if (authCookie && validateAuthToken(authCookie.value, chatId, chatData.password)) {
44+
return true
45+
}
46+
47+
return false
48+
} catch (error) {
49+
logger.error('Error validating chat auth for TTS:', error)
50+
return false
51+
}
52+
}
53+
954
export async function POST(request: NextRequest) {
1055
try {
11-
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
12-
if (!authResult.success) {
13-
logger.error('Authentication failed for TTS stream proxy:', authResult.error)
14-
return new Response('Unauthorized', { status: 401 })
56+
let body: any
57+
try {
58+
body = await request.json()
59+
} catch {
60+
return new Response('Invalid request body', { status: 400 })
1561
}
1662

17-
const body = await request.json()
18-
const { text, voiceId, modelId = 'eleven_turbo_v2_5' } = body
63+
const { text, voiceId, modelId = 'eleven_turbo_v2_5', chatId } = body
64+
65+
if (!chatId) {
66+
return new Response('chatId is required', { status: 400 })
67+
}
1968

2069
if (!text || !voiceId) {
2170
return new Response('Missing required parameters', { status: 400 })
2271
}
2372

73+
const isChatAuthed = await validateChatAuth(request, chatId)
74+
if (!isChatAuthed) {
75+
logger.warn('Chat authentication failed for TTS, chatId:', chatId)
76+
return new Response('Unauthorized', { status: 401 })
77+
}
78+
2479
const voiceIdValidation = validateAlphanumericId(voiceId, 'voiceId', 255)
2580
if (!voiceIdValidation.isValid) {
2681
logger.error(`Invalid voice ID: ${voiceIdValidation.error}`)

apps/sim/app/api/stars/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ export async function GET() {
2323

2424
if (!response.ok) {
2525
console.warn('GitHub API request failed:', response.status)
26-
return NextResponse.json({ stars: formatStarCount(14500) })
26+
return NextResponse.json({ stars: formatStarCount(19400) })
2727
}
2828

2929
const data = await response.json()
30-
return NextResponse.json({ stars: formatStarCount(Number(data?.stargazers_count ?? 14500)) })
30+
return NextResponse.json({ stars: formatStarCount(Number(data?.stargazers_count ?? 19400)) })
3131
} catch (error) {
3232
console.warn('Error fetching GitHub stars:', error)
33-
return NextResponse.json({ stars: formatStarCount(14500) })
33+
return NextResponse.json({ stars: formatStarCount(19400) })
3434
}
3535
}

apps/sim/app/chat/[identifier]/chat.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface ChatConfig {
3939

4040
interface AudioStreamingOptions {
4141
voiceId: string
42+
chatId?: string
4243
onError: (error: Error) => void
4344
}
4445

@@ -62,16 +63,19 @@ function fileToBase64(file: File): Promise<string> {
6263
* Creates an audio stream handler for text-to-speech conversion
6364
* @param streamTextToAudio - Function to stream text to audio
6465
* @param voiceId - The voice ID to use for TTS
66+
* @param chatId - Optional chat ID for deployed chat authentication
6567
* @returns Audio stream handler function or undefined
6668
*/
6769
function createAudioStreamHandler(
6870
streamTextToAudio: (text: string, options: AudioStreamingOptions) => Promise<void>,
69-
voiceId: string
71+
voiceId: string,
72+
chatId?: string
7073
) {
7174
return async (text: string) => {
7275
try {
7376
await streamTextToAudio(text, {
7477
voiceId,
78+
chatId,
7579
onError: (error: Error) => {
7680
logger.error('Audio streaming error:', error)
7781
},
@@ -113,7 +117,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
113117
const [error, setError] = useState<string | null>(null)
114118
const messagesEndRef = useRef<HTMLDivElement>(null)
115119
const messagesContainerRef = useRef<HTMLDivElement>(null)
116-
const [starCount, setStarCount] = useState('3.4k')
120+
const [starCount, setStarCount] = useState('19.4k')
117121
const [conversationId, setConversationId] = useState('')
118122

119123
const [showScrollButton, setShowScrollButton] = useState(false)
@@ -391,7 +395,11 @@ export default function ChatClient({ identifier }: { identifier: string }) {
391395
// Use the streaming hook with audio support
392396
const shouldPlayAudio = isVoiceInput || isVoiceFirstMode
393397
const audioHandler = shouldPlayAudio
394-
? createAudioStreamHandler(streamTextToAudio, DEFAULT_VOICE_SETTINGS.voiceId)
398+
? createAudioStreamHandler(
399+
streamTextToAudio,
400+
DEFAULT_VOICE_SETTINGS.voiceId,
401+
chatConfig?.id
402+
)
395403
: undefined
396404

397405
logger.info('Starting to handle streamed response:', { shouldPlayAudio })

0 commit comments

Comments
 (0)