Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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: 15 additions & 11 deletions apps/sim/app/api/files/multipart/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
type UploadTokenPayload,
verifyUploadToken,
} from '@/lib/uploads/core/upload-token'
import type { StorageConfig } from '@/lib/uploads/shared/types'
import { QUOTA_EXEMPT_STORAGE_CONTEXTS, type StorageConfig } from '@/lib/uploads/shared/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('MultipartUploadAPI')
Expand All @@ -36,7 +36,6 @@ const ALLOWED_UPLOAD_CONTEXTS = new Set<StorageContext>([
'workspace',
'profile-pictures',
'og-images',
'logs',
'workspace-logos',
])

Expand Down Expand Up @@ -135,6 +134,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

const config = getStorageConfig(storageContext)

if (
!QUOTA_EXEMPT_STORAGE_CONTEXTS.has(context as StorageContext) &&
typeof fileSize === 'number'
) {
const { checkStorageQuota } = await import('@/lib/billing/storage')
const quotaCheck = await checkStorageQuota(userId, fileSize)
if (!quotaCheck.allowed) {
return NextResponse.json(
{ error: quotaCheck.error || 'Storage limit exceeded' },
{ status: 413 }
)
}
}

let customKey: string | undefined
if (context === 'workspace' || context === 'mothership') {
const { MAX_WORKSPACE_FILE_SIZE } = await import('@/lib/uploads/shared/types')
Expand All @@ -149,15 +162,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
'@/lib/uploads/contexts/workspace/workspace-file-manager'
)
customKey = generateWorkspaceFileKey(workspaceId, fileName)

const { checkStorageQuota } = await import('@/lib/billing/storage')
const quotaCheck = await checkStorageQuota(userId, fileSize)
if (!quotaCheck.allowed) {
return NextResponse.json(
{ error: quotaCheck.error || 'Storage limit exceeded' },
{ status: 413 }
)
}
} else if (context === 'execution') {
const workflowId = (data as { workflowId?: unknown }).workflowId
const executionId = (data as { executionId?: unknown }).executionId
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/api/tools/sharepoint/site/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => {

const accountRow = credentials[0]

if (!resolved.workspaceId && accountRow.userId !== session.user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/ssh/read-file-content/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

const content = await new Promise<string>((resolve, reject) => {
const chunks: Buffer[] = []
let totalBytes = 0
const readStream = sftp.createReadStream(filePath)

readStream.on('data', (chunk: Buffer) => {
totalBytes += chunk.length
if (totalBytes > maxBytes) {
readStream.destroy()
reject(new Error(`File exceeds maximum allowed size of ${params.maxSize}MB`))
return
}
chunks.push(chunk)
})

Expand Down
16 changes: 16 additions & 0 deletions apps/sim/app/api/workflows/[id]/log/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { db } from '@sim/db'
import { workflowExecutionLogs } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { workflowLogContract } from '@/lib/api/contracts/workflows'
import { parseRequest } from '@/lib/api/server'
Expand Down Expand Up @@ -40,6 +43,19 @@ export const POST = withRouteHandler(
return createErrorResponse('executionId is required when logging results', 400)
}

const [existingLog] = await db
.select({ workflowId: workflowExecutionLogs.workflowId })
.from(workflowExecutionLogs)
.where(eq(workflowExecutionLogs.executionId, executionId))
.limit(1)

if (existingLog && existingLog.workflowId !== id) {
logger.warn(
`[${requestId}] executionId ${executionId} belongs to workflow ${existingLog.workflowId}, not ${id}`
)
return createErrorResponse('Execution not found', 404)
}

logger.info(`[${requestId}] Persisting execution result for workflow: ${id}`, {
executionId,
success: result.success,
Expand Down
8 changes: 8 additions & 0 deletions apps/sim/lib/environment/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getAccessibleEnvCredentials,
syncPersonalEnvCredentialsForUser,
} from '@/lib/credentials/environment'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('EnvironmentUtils')
const EFFECTIVE_ENV_CACHE_TTL_MS = 15_000
Expand Down Expand Up @@ -72,6 +73,13 @@ export async function getPersonalAndWorkspaceEnv(
conflicts: string[]
decryptionFailures: string[]
}> {
if (workspaceId) {
const access = await checkWorkspaceAccess(workspaceId, userId)
if (!access.hasAccess) {
throw new Error(`Access denied to workspace ${workspaceId}`)
}
}

const [personalRows, workspaceRows, accessibleEnvCredentials] = await Promise.all([
db.select().from(environment).where(eq(environment.userId, userId)).limit(1),
workspaceId
Expand Down
39 changes: 33 additions & 6 deletions apps/sim/lib/logs/execution/logging-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { db } from '@sim/db'
import { workflowExecutionLogs } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { eq, sql } from 'drizzle-orm'
import { and, eq, sql } from 'drizzle-orm'
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
import { executionLogger } from '@/lib/logs/execution/logger'
import {
Expand All @@ -29,6 +29,7 @@ type TriggerData = Record<string, unknown> & {

function buildStartedMarkerPersistenceQuery(params: {
executionId: string
workflowId: string
marker: ExecutionLastStartedBlock
}) {
const markerJson = JSON.stringify(params.marker)
Expand All @@ -41,6 +42,7 @@ function buildStartedMarkerPersistenceQuery(params: {
true
)
WHERE execution_id = ${params.executionId}
AND workflow_id = ${params.workflowId}
AND COALESCE(
jsonb_extract_path_text(COALESCE(execution_data, '{}'::jsonb), 'lastStartedBlock', 'startedAt'),
''
Expand All @@ -49,6 +51,7 @@ function buildStartedMarkerPersistenceQuery(params: {

function buildCompletedMarkerPersistenceQuery(params: {
executionId: string
workflowId: string
marker: ExecutionLastCompletedBlock
}) {
const markerJson = JSON.stringify(params.marker)
Expand All @@ -61,6 +64,7 @@ function buildCompletedMarkerPersistenceQuery(params: {
true
)
WHERE execution_id = ${params.executionId}
AND workflow_id = ${params.workflowId}
AND COALESCE(
jsonb_extract_path_text(COALESCE(execution_data, '{}'::jsonb), 'lastCompletedBlock', 'endedAt'),
''
Expand Down Expand Up @@ -190,6 +194,7 @@ export class LoggingSession {
await db.execute(
buildStartedMarkerPersistenceQuery({
executionId: this.executionId,
workflowId: this.workflowId,
marker,
})
)
Expand All @@ -205,6 +210,7 @@ export class LoggingSession {
await db.execute(
buildCompletedMarkerPersistenceQuery({
executionId: this.executionId,
workflowId: this.workflowId,
marker,
})
)
Expand Down Expand Up @@ -357,7 +363,12 @@ export class LoggingSession {
models: this.accumulatedCost.models,
},
})
.where(eq(workflowExecutionLogs.executionId, this.executionId))
.where(
and(
eq(workflowExecutionLogs.workflowId, this.workflowId),
eq(workflowExecutionLogs.executionId, this.executionId)
)
)

this.costFlushed = true
} catch (error) {
Expand All @@ -372,7 +383,12 @@ export class LoggingSession {
const [existing] = await db
.select({ cost: workflowExecutionLogs.cost })
.from(workflowExecutionLogs)
.where(eq(workflowExecutionLogs.executionId, this.executionId))
.where(
and(
eq(workflowExecutionLogs.workflowId, this.workflowId),
eq(workflowExecutionLogs.executionId, this.executionId)
)
)
Comment thread
waleedlatif1 marked this conversation as resolved.
.limit(1)

if (existing?.cost) {
Expand Down Expand Up @@ -1064,13 +1080,19 @@ export class LoggingSession {

async markAsFailed(errorMessage?: string): Promise<void> {
await this.waitForCompletion()
await LoggingSession.markExecutionAsFailed(this.executionId, errorMessage, this.requestId)
await LoggingSession.markExecutionAsFailed(
this.executionId,
errorMessage,
this.requestId,
this.workflowId
)
}

static async markExecutionAsFailed(
executionId: string,
errorMessage?: string,
requestId?: string
requestId?: string,
workflowId?: string
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
): Promise<void> {
try {
const message = errorMessage || 'Run failed'
Expand All @@ -1093,7 +1115,12 @@ export class LoggingSession {
to_jsonb('force_failed'::text)
)`,
})
.where(eq(workflowExecutionLogs.executionId, executionId))
.where(
and(
eq(workflowExecutionLogs.executionId, executionId),
workflowId ? eq(workflowExecutionLogs.workflowId, workflowId) : undefined
)
)

logger.info(`[${requestId || 'unknown'}] Marked execution ${executionId} as failed`)
} catch (error) {
Expand Down
12 changes: 12 additions & 0 deletions apps/sim/lib/uploads/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export type StorageContext =
| 'logs'
| 'workspace-logos'

/**
* Contexts exempt from storage quota checks — small metadata assets not managed
* by the user (profile pictures, logos, OG images). All other contexts represent
* user-driven uploads and must pass quota validation before upload is initiated.
*/
export const QUOTA_EXEMPT_STORAGE_CONTEXTS = new Set<StorageContext>([
'profile-pictures',
'workspace-logos',
'og-images',
'logs',
])
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
waleedlatif1 marked this conversation as resolved.

export interface FileInfo {
path: string
key: string
Expand Down
8 changes: 6 additions & 2 deletions apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,9 @@ export class PauseResumeManager {
})
await LoggingSession.markExecutionAsFailed(
effectiveExecutionId,
'Missing snapshot seed for paused execution'
'Missing snapshot seed for paused execution',
undefined,
pausedExecution.workflowId
)
} else {
try {
Expand All @@ -462,7 +464,9 @@ export class PauseResumeManager {
})
await LoggingSession.markExecutionAsFailed(
effectiveExecutionId,
`Failed to persist pause state: ${toError(pauseError).message}`
`Failed to persist pause state: ${toError(pauseError).message}`,
undefined,
pausedExecution.workflowId
)
}
}
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/tools/supabase/vector_search.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type {
SupabaseVectorSearchParams,
SupabaseVectorSearchResponse,
Expand Down Expand Up @@ -56,8 +57,9 @@ export const vectorSearchTool: ToolConfig<

request: {
url: (params) => {
// Use RPC endpoint for calling PostgreSQL functions
return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${params.functionName}`
const fnValidation = validateDatabaseIdentifier(params.functionName, 'functionName')
if (!fnValidation.isValid) throw new Error(fnValidation.error)
return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${encodeURIComponent(params.functionName)}`
},
method: 'POST',
headers: (params) => ({
Expand Down
5 changes: 2 additions & 3 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"includes": [
"**",
"!**/.next",
"!**/.next/**",
"!**/.next",
Comment thread
waleedlatif1 marked this conversation as resolved.
"!**/next-env.d.ts",
"!**/out",
"!**/dist",
Expand Down Expand Up @@ -69,8 +69,7 @@
"rules": {
"recommended": true,
"nursery": {
"useSortedClasses": "warn",
"noNestedComponentDefinitions": "off"
"useSortedClasses": "warn"
},
"a11y": {
"noSvgWithoutTitle": "off",
Expand Down
Loading