Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
219a065
v0.4.12: guardrails, mistral models, privacy policy updates (#1608)
waleedlatif1 Oct 12, 2025
7f82ed3
v0.4.13: bugfixes for dev containers, posthog redirect, helm updates
icecrasher321 Oct 13, 2025
fb0fa1f
v0.4.14: canvas speedup and copilot context window
Sg312 Oct 14, 2025
2bc8c7b
v0.4.15: helm chart updates, telegram tools, youtube tools, file uplo…
waleedlatif1 Oct 15, 2025
04f109c
v0.4.16: executions dashboard, UI fixes, zep tools, slack fixes
icecrasher321 Oct 16, 2025
da091df
v0.4.17: input format + files support for webhooks, docs updates, das…
waleedlatif1 Oct 16, 2025
e4ddeb0
v0.4.18: file upload tools, copilot upgrade, docs changes, model filt…
icecrasher321 Oct 19, 2025
641e353
v0.4.19: landing page fix
icecrasher321 Oct 19, 2025
9751c9f
v0.4.20: internal request, kb url fixes, docs styling
icecrasher321 Oct 21, 2025
1b7437a
v0.4.21: more internal auth changes, supabase vector search tool
icecrasher321 Oct 22, 2025
71ae27b
v0.4.22: fix execution context pass for google sheets
icecrasher321 Oct 22, 2025
9b2490c
v0.4.23: webflow tools + triggers, copilot api key fix (#1723)
waleedlatif1 Oct 23, 2025
7f1ff7f
fix(billing): should allow restoring subscription (#1728)
icecrasher321 Oct 25, 2025
a02016e
v0.4.24: sso for chat deployment, usage indicator for file storage, m…
icecrasher321 Oct 27, 2025
9a4b9e2
v0.4.25: variables block, sort ordering for kb, careers page, storage…
waleedlatif1 Oct 29, 2025
69073a3
- Replace `await import` with `require` for improved compatibility in…
web-flow Oct 30, 2025
354f766
Merge remote-tracking branch 'refs/remotes/origin/main' into my-fork-…
TonyCasey Nov 20, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
import { useEffect, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import {
Button,
Modal,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTitle,
} from '@/components/emcn'
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { useSession, useSubscription } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
import { getSubscriptionStatus } from '@/lib/subscription/helpers'
Expand Down Expand Up @@ -137,8 +143,9 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
let subscriptionId: string | undefined

if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) {
const orgSubscription = useOrganizationStore.getState().subscriptionData
referenceId = activeOrgId
subscriptionId = subData?.data?.id
subscriptionId = orgSubscription?.id
} else {
// For personal subscriptions, use user ID and let better-auth find the subscription
referenceId = session.user.id
Expand All @@ -152,12 +159,24 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
if (subscriptionId) {
restoreParams.subscriptionId = subscriptionId
}
}

logger.info('Restoring subscription', { referenceId, subscriptionId })

// Build restore params - only include subscriptionId if we have one (team/enterprise)
const restoreParams: any = { referenceId }
if (subscriptionId) {
restoreParams.subscriptionId = subscriptionId
}
}

const result = await betterAuthSubscription.restore(restoreParams)

logger.info('Subscription restored successfully', result)
}

// Refresh state and close
await refresh()
// Invalidate queries to refresh data
await queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
if (activeOrgId) {
Expand Down Expand Up @@ -225,8 +244,12 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
onClick={() => setIsDialogOpen(true)}
disabled={isLoading}
className={cn(
'h-8 rounded-[8px] font-medium text-xs',
error && 'border-red-500 text-red-500 dark:border-red-500 dark:text-red-500'
'h-8 rounded-[8px] font-medium text-xs transition-all duration-200',
error
? 'border-red-500 text-red-500 dark:border-red-500 dark:text-red-500'
: isCancelAtPeriodEnd
? 'text-muted-foreground hover:border-green-500 hover:bg-green-500 hover:text-white dark:hover:border-green-500 dark:hover:bg-green-500'
: 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500'
)}
>
{error ? 'Error' : isCancelAtPeriodEnd ? 'Restore' : 'Manage'}
Expand Down Expand Up @@ -280,13 +303,13 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
const subscriptionStatus = currentSubscriptionStatus
if (subscriptionStatus.isPaid && isCancelAtPeriodEnd) {
return (
<Button
<AlertDialogAction
onClick={handleKeep}
className='h-[32px] bg-green-500 px-[12px] text-white hover:bg-green-600 dark:bg-green-500 dark:hover:bg-green-600'
className='h-9 w-full rounded-[8px] bg-green-500 text-white transition-all duration-200 hover:bg-green-600 dark:bg-green-500 dark:hover:bg-green-600'
disabled={isLoading}
>
{isLoading ? 'Restoring...' : 'Restore Subscription'}
</Button>
</AlertDialogAction>
)
}
return (
Expand Down
11 changes: 8 additions & 3 deletions apps/sim/lib/uploads/core/setup.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { existsSync } from 'fs'
import { mkdir } from 'fs/promises'
import path, { join } from 'path'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getStorageProvider, USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads/config'
Expand All @@ -24,7 +21,15 @@ export async function ensureUploadsDirectory() {
return true
}

if (typeof window !== 'undefined' || !UPLOAD_DIR_SERVER) {
// Skip on client side
return true
}
Comment on lines +24 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The !UPLOAD_DIR_SERVER check might be redundant since UPLOAD_DIR_SERVER is only undefined when typeof window !== 'undefined'. Is there a scenario where UPLOAD_DIR_SERVER could be undefined on the server side?

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/lib/uploads/core/setup.server.ts
Line: 33:36

Comment:
**logic:** The `!UPLOAD_DIR_SERVER` check might be redundant since `UPLOAD_DIR_SERVER` is only undefined when `typeof window !== 'undefined'`. Is there a scenario where UPLOAD_DIR_SERVER could be undefined on the server side?

How can I resolve this? If you propose a fix, please make it concise.


try {
const { existsSync } = require('fs')
const { mkdir } = require('fs/promises')

if (!existsSync(UPLOAD_DIR_SERVER)) {
await mkdir(UPLOAD_DIR_SERVER, { recursive: true })
} else {
Expand Down
249 changes: 248 additions & 1 deletion apps/sim/lib/uploads/core/storage-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,254 @@
import { USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads/config'
import type { StorageConfig } from '@/lib/uploads/shared/types'

export type { StorageConfig } from '@/lib/uploads/shared/types'
import { createLogger } from '@/lib/logs/console/logger'
import { USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads/core/setup'
import type { CustomBlobConfig } from '@/lib/uploads/providers/blob/blob-client'
import type { CustomS3Config } from '@/lib/uploads/providers/s3/s3-client'

const logger = createLogger('StorageClient')

// Client-safe type definitions
export type FileInfo = {
path: string
key: string
name: string
size: number
type: string
}

export type CustomStorageConfig = {
// S3 config
bucket?: string
region?: string
// Blob config
containerName?: string
accountName?: string
accountKey?: string
connectionString?: string
}

/**
* Upload a file to the configured storage provider
* @param file Buffer containing file data
* @param fileName Original file name
* @param contentType MIME type of the file
* @param size File size in bytes (optional, will use buffer length if not provided)
* @returns Object with file information
*/
export async function uploadFile(
file: Buffer,
fileName: string,
contentType: string,
size?: number
): Promise<FileInfo>

/**
* Upload a file to the configured storage provider with custom configuration
* @param file Buffer containing file data
* @param fileName Original file name
* @param contentType MIME type of the file
* @param customConfig Custom storage configuration
* @param size File size in bytes (optional, will use buffer length if not provided)
* @returns Object with file information
*/
export async function uploadFile(
file: Buffer,
fileName: string,
contentType: string,
customConfig: CustomStorageConfig,
size?: number
): Promise<FileInfo>

export async function uploadFile(
file: Buffer,
fileName: string,
contentType: string,
configOrSize?: CustomStorageConfig | number,
size?: number
): Promise<FileInfo> {
if (USE_BLOB_STORAGE) {
logger.info(`Uploading file to Azure Blob Storage: ${fileName}`)
const { uploadToBlob } = await import('@/lib/uploads/providers/blob/blob-client')
if (typeof configOrSize === 'object') {
const blobConfig: CustomBlobConfig = {
containerName: configOrSize.containerName!,
accountName: configOrSize.accountName!,
accountKey: configOrSize.accountKey,
connectionString: configOrSize.connectionString,
}
return uploadToBlob(file, fileName, contentType, blobConfig, size)
}
return uploadToBlob(file, fileName, contentType, configOrSize)
}

if (USE_S3_STORAGE) {
logger.info(`Uploading file to S3: ${fileName}`)
const { uploadToS3 } = await import('@/lib/uploads/providers/s3/s3-client')
if (typeof configOrSize === 'object') {
const s3Config: CustomS3Config = {
bucket: configOrSize.bucket!,
region: configOrSize.region!,
}
return uploadToS3(file, fileName, contentType, s3Config, size)
}
return uploadToS3(file, fileName, contentType, configOrSize)
}

if (typeof window !== 'undefined') {
throw new Error('Local file upload is only supported on the server')
}

logger.info(`Uploading file to local storage: ${fileName}`)
const { writeFile } = require('fs/promises')
const { join } = require('path')
const { v4: uuidv4 } = require('uuid')
const { UPLOAD_DIR_SERVER } = require('@/lib/uploads/core/setup.server')

const safeFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_').replace(/\.\./g, '')
const uniqueKey = `${uuidv4()}-${safeFileName}`
const filePath = join(UPLOAD_DIR_SERVER, uniqueKey)

try {
await writeFile(filePath, file)
} catch (error) {
logger.error(`Failed to write file to local storage: ${fileName}`, error)
throw new Error(
`Failed to write file to local storage: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}

const fileSize = typeof configOrSize === 'number' ? configOrSize : size || file.length

return {
path: `/api/files/serve/${uniqueKey}`,
key: uniqueKey,
name: fileName,
size: fileSize,
type: contentType,
}
}

/**
* Download a file from the configured storage provider
* @param key File key/name
* @returns File buffer
*/
export async function downloadFile(key: string): Promise<Buffer>

/**
* Download a file from the configured storage provider with custom configuration
* @param key File key/name
* @param customConfig Custom storage configuration
* @returns File buffer
*/
export async function downloadFile(key: string, customConfig: CustomStorageConfig): Promise<Buffer>

export async function downloadFile(
key: string,
customConfig?: CustomStorageConfig
): Promise<Buffer> {
if (USE_BLOB_STORAGE) {
logger.info(`Downloading file from Azure Blob Storage: ${key}`)
const { downloadFromBlob } = await import('@/lib/uploads/providers/blob/blob-client')
if (customConfig) {
const blobConfig: CustomBlobConfig = {
containerName: customConfig.containerName!,
accountName: customConfig.accountName!,
accountKey: customConfig.accountKey,
connectionString: customConfig.connectionString,
}
return downloadFromBlob(key, blobConfig)
}
return downloadFromBlob(key)
}

if (USE_S3_STORAGE) {
logger.info(`Downloading file from S3: ${key}`)
const { downloadFromS3 } = await import('@/lib/uploads/providers/s3/s3-client')
if (customConfig) {
const s3Config: CustomS3Config = {
bucket: customConfig.bucket!,
region: customConfig.region!,
}
return downloadFromS3(key, s3Config)
}
return downloadFromS3(key)
}

if (typeof window !== 'undefined') {
throw new Error('Local file download is only supported on the server')
}

logger.info(`Downloading file from local storage: ${key}`)
const { readFile } = require('fs/promises')
const { join, resolve, sep } = require('path')
const { UPLOAD_DIR_SERVER } = require('@/lib/uploads/core/setup.server')

const safeKey = key.replace(/\.\./g, '').replace(/[/\\]/g, '')
const filePath = join(UPLOAD_DIR_SERVER, safeKey)

const resolvedPath = resolve(filePath)
const allowedDir = resolve(UPLOAD_DIR_SERVER)
if (!resolvedPath.startsWith(allowedDir + sep) && resolvedPath !== allowedDir) {
throw new Error('Invalid file path')
}

try {
return await readFile(filePath)
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(`File not found: ${key}`)
}
throw error
}
}

/**
* Delete a file from the configured storage provider
* @param key File key/name
*/
export async function deleteFile(key: string): Promise<void> {
if (USE_BLOB_STORAGE) {
logger.info(`Deleting file from Azure Blob Storage: ${key}`)
const { deleteFromBlob } = await import('@/lib/uploads/providers/blob/blob-client')
return deleteFromBlob(key)
}

if (USE_S3_STORAGE) {
logger.info(`Deleting file from S3: ${key}`)
const { deleteFromS3 } = await import('@/lib/uploads/providers/s3/s3-client')
return deleteFromS3(key)
}

if (typeof window !== 'undefined') {
throw new Error('Local file deletion is only supported on the server')
}

logger.info(`Deleting file from local storage: ${key}`)
const { unlink } = require('fs/promises')
const { join, resolve, sep } = require('path')
const { UPLOAD_DIR_SERVER } = require('@/lib/uploads/core/setup.server')

const safeKey = key.replace(/\.\./g, '').replace(/[/\\]/g, '')
const filePath = join(UPLOAD_DIR_SERVER, safeKey)

const resolvedPath = resolve(filePath)
const allowedDir = resolve(UPLOAD_DIR_SERVER)
if (!resolvedPath.startsWith(allowedDir + sep) && resolvedPath !== allowedDir) {
throw new Error('Invalid file path')
}

try {
await unlink(filePath)
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
logger.warn(`File not found during deletion: ${key}`)
return
}
throw error
}
}

/**
* Get the current storage provider name
Expand Down
Loading