Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion apps/sim/app/api/help/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@ import { helpFormBodySchema } from '@/lib/api/contracts/common'
import { validationErrorResponse } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import {
isPayloadSizeLimitError,
MAX_MULTIPART_OVERHEAD_BYTES,
readFormDataWithLimit,
} from '@/lib/core/utils/stream-limits'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { getFromEmailAddress, getHelpEmailAddress } from '@/lib/messaging/email/utils'
import { MAX_WORKSPACE_FORMDATA_FILE_SIZE } from '@/lib/uploads/shared/types'

const logger = createLogger('HelpAPI')

/**
* The form can carry several image attachments with no server-side count
* cap, so this reuses the repo's largest existing per-request form-data
* bound (see files/upload route) rather than an arbitrary smaller limit
* that could reject a legitimate multi-image submission.
*/
const MAX_HELP_FORM_BYTES = MAX_WORKSPACE_FORMDATA_FILE_SIZE + MAX_MULTIPART_OVERHEAD_BYTES

export const POST = withRouteHandler(async (req: NextRequest) => {
const requestId = generateRequestId()

Expand All @@ -23,7 +37,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {

const email = session.user.email

const formData = await req.formData()
const formData = await readFormDataWithLimit(req, {
maxBytes: MAX_HELP_FORM_BYTES,
label: 'Help request form data',
})

const subject = formData.get('subject') as string
const message = formData.get('message') as string
Expand Down Expand Up @@ -128,6 +145,13 @@ ${message}
{ status: 200 }
)
} catch (error) {
if (isPayloadSizeLimitError(error)) {
logger.warn(`[${requestId}] Help request form data too large`, { message: error.message })
return NextResponse.json(
{ error: `Request body exceeds the maximum allowed size of ${MAX_HELP_FORM_BYTES} bytes` },
{ status: 413 }
)
}
if (error instanceof Error && error.message.includes('not configured')) {
logger.error(`[${requestId}] Email service configuration error`, error)
return NextResponse.json(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
createChunkBodySchema,
listKnowledgeChunksQuerySchema,
} from '@/lib/api/contracts/knowledge'
import { isZodError, parseRequest } from '@/lib/api/server'
import { isZodError, parseJsonBody, parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
Expand Down Expand Up @@ -103,17 +103,21 @@ export const POST = withRouteHandler(
const { id: knowledgeBaseId, documentId } = await params

try {
const body = await req.json()
const { workflowId, ...searchParams } = body

const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Authentication failed: ${auth.error || 'Unauthorized'}`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = auth.userId

const parsedBody = await parseJsonBody(req)
if (!parsedBody.success) return parsedBody.response
const { workflowId, ...searchParams } = parsedBody.data as Record<string, unknown>

if (workflowId) {
if (typeof workflowId !== 'string') {
return NextResponse.json({ error: 'workflowId must be a string' }, { status: 400 })
}
const authorization = await authorizeWorkflowByWorkspacePermission({
workflowId,
userId,
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/app/api/speech/token/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,13 @@ describe('POST /api/speech/token — usage attribution', () => {
workspaceId: 'ws-1',
})
})

it('rejects an oversized body before any auth/billing work runs', async () => {
const oversizedBody = { chatId: 'x'.repeat(64 * 1024) }
const res = await POST(createMockRequest('POST', oversizedBody))

expect(res.status).toBe(413)
expect(mockGetSession).not.toHaveBeenCalled()
expect(mockRecordUsage).not.toHaveBeenCalled()
})
})
13 changes: 11 additions & 2 deletions apps/sim/app/api/speech/token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getErrorMessage } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { speechTokenBodySchema } from '@/lib/api/contracts/media/speech'
import { parseOptionalJsonBody } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { checkActorUsageLimits } from '@/lib/billing/calculations/usage-monitor'
import { recordUsage } from '@/lib/billing/core/usage-log'
Expand Down Expand Up @@ -34,6 +35,13 @@ const STT_TOKEN_RATE_LIMIT = {
refillIntervalMs: 72 * 1000,
} as const

/**
* This body only ever carries an optional chatId/workspaceId string, so a
* tight cap keeps an unauthenticated caller from forcing a large in-memory
* allocation before the auth checks below run.
*/
const MAX_SPEECH_TOKEN_BODY_BYTES = 16 * 1024

function hashVoiceToken(token: string): string {
return createHash('sha256').update(token).digest('hex')
}
Expand Down Expand Up @@ -87,8 +95,9 @@ async function validateChatAuth(

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const rawBody = await request.json().catch(() => ({}))
const body = speechTokenBodySchema.safeParse(rawBody)
const parsedBody = await parseOptionalJsonBody(request, MAX_SPEECH_TOKEN_BODY_BYTES)
if (!parsedBody.success) return parsedBody.response
const body = speechTokenBodySchema.safeParse(parsedBody.data ?? {})
const chatId =
body.success && typeof body.data.chatId === 'string' ? body.data.chatId : undefined

Expand Down
Loading