Skip to content
Prev Previous commit
Next Next commit
use shared getclientip helper, fix deployed chat
  • Loading branch information
icecrasher321 committed Apr 8, 2026
commit 11b6c59cf589541df14fa275a19a3244cc081eef
6 changes: 3 additions & 3 deletions apps/sim/app/api/a2a/serve/[agentId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { type AuthResult, AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { getClientIp } from '@/lib/core/utils/request'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
Expand Down Expand Up @@ -52,10 +53,9 @@ function getCallerFingerprint(request: NextRequest, userId?: string | null): str
return `user:${userId}`
}

const forwardedFor = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
const realIp = request.headers.get('x-real-ip')?.trim()
const clientIp = getClientIp(request)
const userAgent = request.headers.get('user-agent')?.trim() || 'unknown'
return `public:${forwardedFor || realIp || 'unknown'}:${userAgent}`
return `public:${clientIp}:${userAgent}`
}

function hasCallerAccessToTask(
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/demo-requests/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { env } from '@/lib/core/config/env'
import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateRequestId, getClientIp } from '@/lib/core/utils/request'
import { getEmailDomain } from '@/lib/core/utils/urls'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
Expand All @@ -25,7 +25,7 @@ export async function POST(req: NextRequest) {
const requestId = generateRequestId()

try {
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
const ip = getClientIp(req)
const storageKey = `public:demo-request:${ip}`

const { allowed, remaining, resetAt } = await rateLimiter.checkRateLimitDirect(
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/help/integration-request/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { z } from 'zod'
import { env } from '@/lib/core/config/env'
import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateRequestId, getClientIp } from '@/lib/core/utils/request'
import { getEmailDomain } from '@/lib/core/utils/urls'
import { sendEmail } from '@/lib/messaging/email/mailer'
import {
Expand Down Expand Up @@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
const requestId = generateRequestId()

try {
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
const ip = getClientIp(req)
const storageKey = `public:integration-request:${ip}`

const { allowed, remaining, resetAt } = await rateLimiter.checkRateLimitDirect(
Expand Down
11 changes: 5 additions & 6 deletions apps/sim/app/api/settings/voice/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { hasSTTService } from '@/lib/speech/transcriber'

/**
* Returns whether server-side STT is configured.
* Unauthenticated — the response is a single boolean,
* not sensitive data, and deployed chat visitors need it.
*/
export async function GET() {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

return NextResponse.json({ sttAvailable: hasSTTService() })
}
47 changes: 23 additions & 24 deletions apps/sim/app/api/speech/token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { recordUsage } from '@/lib/billing/core/usage-log'
import { env } from '@/lib/core/config/env'
import { getCostMultiplier } from '@/lib/core/config/feature-flags'
import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/feature-flags'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { validateAuthToken } from '@/lib/core/security/deployment'
import { getClientIp } from '@/lib/core/utils/request'

const logger = createLogger('SpeechTokenAPI')

Expand All @@ -21,9 +22,9 @@ const VOICE_SESSION_MAX_MINUTES = 3
const VOICE_SESSION_COST = VOICE_SESSION_COST_PER_MIN * VOICE_SESSION_MAX_MINUTES

const STT_TOKEN_RATE_LIMIT = {
maxTokens: 20,
refillRate: 150,
refillIntervalMs: 60 * 60 * 1000,
maxTokens: 30,
refillRate: 3,
refillIntervalMs: 72 * 1000,
} as const

const rateLimiter = new RateLimiter()
Expand Down Expand Up @@ -72,6 +73,7 @@ export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}))
const chatId = body?.chatId as string | undefined
const skipBilling = body?.skipBilling === true
Comment thread
icecrasher321 marked this conversation as resolved.
Outdated

let billingUserId: string | undefined

Expand All @@ -89,26 +91,23 @@ export async function POST(request: NextRequest) {
billingUserId = session.user.id
}

const clientIp =
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
request.headers.get('x-real-ip')?.trim() ||
'unknown'
if (isBillingEnabled) {
const rateLimitKey = chatId
? `stt-token:chat:${chatId}:${getClientIp(request)}`
: `stt-token:user:${billingUserId}`

const rateLimitKey = chatId
? `stt-token:chat:${chatId}:${clientIp}`
: `stt-token:user:${billingUserId}`

const rateCheck = await rateLimiter.checkRateLimitDirect(rateLimitKey, STT_TOKEN_RATE_LIMIT)
if (!rateCheck.allowed) {
return NextResponse.json(
{ error: 'Voice input rate limit exceeded. Please try again later.' },
{
status: 429,
headers: {
'Retry-After': String(Math.ceil((rateCheck.retryAfterMs ?? 60000) / 1000)),
},
}
)
const rateCheck = await rateLimiter.checkRateLimitDirect(rateLimitKey, STT_TOKEN_RATE_LIMIT)
if (!rateCheck.allowed) {
return NextResponse.json(
{ error: 'Voice input rate limit exceeded. Please try again later.' },
{
status: 429,
headers: {
'Retry-After': String(Math.ceil((rateCheck.retryAfterMs ?? 60000) / 1000)),
},
}
)
}
}

const apiKey = env.ELEVENLABS_API_KEY
Expand All @@ -134,7 +133,7 @@ export async function POST(request: NextRequest) {

const data = await response.json()

if (billingUserId) {
if (billingUserId && !skipBilling) {
await recordUsage({
userId: billingUserId,
entries: [
Expand Down
12 changes: 11 additions & 1 deletion apps/sim/app/chat/[identifier]/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ export default function ChatClient({ identifier }: { identifier: string }) {
const [authRequired, setAuthRequired] = useState<'password' | 'email' | 'sso' | null>(null)

const [isVoiceFirstMode, setIsVoiceFirstMode] = useState(false)
const [sttAvailable, setSttAvailable] = useState(false)

useEffect(() => {
fetch('/api/settings/voice')
.then((r) => (r.ok ? r.json() : { sttAvailable: false }))
.then((data) => setSttAvailable(data.sttAvailable === true))
.catch(() => setSttAvailable(false))
}, [])
const { isStreamingResponse, abortControllerRef, stopStreaming, handleStreamedResponse } =
useChatStreaming()
const audioContextRef = useRef<AudioContext | null>(null)
Expand Down Expand Up @@ -443,8 +451,9 @@ export default function ChatClient({ identifier }: { identifier: string }) {
}, [isStreamingResponse, stopStreaming, setMessages, stopAudio])

const handleVoiceStart = useCallback(() => {
if (!sttAvailable) return
setIsVoiceFirstMode(true)
}, [])
}, [sttAvailable])

const handleExitVoiceMode = useCallback(() => {
setIsVoiceFirstMode(false)
Expand Down Expand Up @@ -530,6 +539,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
isStreaming={isStreamingResponse}
onStopStreaming={() => stopStreaming(setMessages)}
onVoiceStart={handleVoiceStart}
sttAvailable={sttAvailable}
/>
</div>
</div>
Expand Down
22 changes: 11 additions & 11 deletions apps/sim/app/chat/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ const logger = createLogger('ChatInput')

const MAX_TEXTAREA_HEIGHT = 200

const IS_STT_AVAILABLE =
typeof window !== 'undefined' &&
!!(
(window as Window & { SpeechRecognition?: unknown; webkitSpeechRecognition?: unknown })
.SpeechRecognition ||
(window as Window & { webkitSpeechRecognition?: unknown }).webkitSpeechRecognition
)

interface AttachedFile {
id: string
name: string
Expand All @@ -37,7 +29,15 @@ export const ChatInput: React.FC<{
onStopStreaming?: () => void
onVoiceStart?: () => void
voiceOnly?: boolean
}> = ({ onSubmit, isStreaming = false, onStopStreaming, onVoiceStart, voiceOnly = false }) => {
sttAvailable?: boolean
}> = ({
onSubmit,
isStreaming = false,
onStopStreaming,
onVoiceStart,
voiceOnly = false,
sttAvailable = false,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const [inputValue, setInputValue] = useState('')
Expand Down Expand Up @@ -142,7 +142,7 @@ export const ChatInput: React.FC<{
return (
<Tooltip.Provider>
<div className='flex items-center justify-center'>
{IS_STT_AVAILABLE && (
{sttAvailable && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<div>
Expand Down Expand Up @@ -295,7 +295,7 @@ export const ChatInput: React.FC<{

{/* Right: mic + send */}
<div className='flex items-center gap-1.5'>
{IS_STT_AVAILABLE && (
{sttAvailable && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
Expand Down
49 changes: 1 addition & 48 deletions apps/sim/app/chat/components/input/voice-input.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,9 @@
'use client'

import { useCallback, useEffect, useState } from 'react'
import { useCallback } from 'react'
import { motion } from 'framer-motion'
import { Mic } from 'lucide-react'

interface SpeechRecognitionEvent extends Event {
resultIndex: number
results: SpeechRecognitionResultList
}

interface SpeechRecognitionErrorEvent extends Event {
error: string
message?: string
}

interface SpeechRecognition extends EventTarget {
continuous: boolean
interimResults: boolean
lang: string
start(): void
stop(): void
abort(): void
onstart: ((this: SpeechRecognition, ev: Event) => any) | null
onend: ((this: SpeechRecognition, ev: Event) => any) | null
onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null
onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any) | null
}

interface SpeechRecognitionStatic {
new (): SpeechRecognition
}

type WindowWithSpeech = Window & {
SpeechRecognition?: SpeechRecognitionStatic
webkitSpeechRecognition?: SpeechRecognitionStatic
}

interface VoiceInputProps {
onVoiceStart: () => void
isListening?: boolean
Expand All @@ -51,24 +19,11 @@ export function VoiceInput({
large = false,
minimal = false,
}: VoiceInputProps) {
const [isSupported, setIsSupported] = useState(false)

// Check if speech recognition is supported
useEffect(() => {
const w = window as WindowWithSpeech
const SpeechRecognitionCtor = w.SpeechRecognition || w.webkitSpeechRecognition
setIsSupported(!!SpeechRecognitionCtor)
}, [])

const handleVoiceClick = useCallback(() => {
if (disabled) return
onVoiceStart()
}, [disabled, onVoiceStart])

if (!isSupported) {
return null
}

if (minimal) {
return (
<button
Expand All @@ -88,7 +43,6 @@ export function VoiceInput({
if (large) {
return (
<div className='flex flex-col items-center'>
{/* Large Voice Button */}
<motion.button
type='button'
onClick={handleVoiceClick}
Expand All @@ -110,7 +64,6 @@ export function VoiceInput({

return (
<div className='flex items-center'>
{/* Voice Button - Now matches send button styling */}
<motion.button
type='button'
onClick={handleVoiceClick}
Expand Down
Loading