diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx
index 293b7aa97d..0649ddd27a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx
@@ -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'
@@ -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
@@ -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) {
@@ -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'}
@@ -280,13 +303,13 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
const subscriptionStatus = currentSubscriptionStatus
if (subscriptionStatus.isPaid && isCancelAtPeriodEnd) {
return (
-
+
)
}
return (
diff --git a/apps/sim/lib/uploads/core/setup.server.ts b/apps/sim/lib/uploads/core/setup.server.ts
index 36b1a12206..5210c422aa 100644
--- a/apps/sim/lib/uploads/core/setup.server.ts
+++ b/apps/sim/lib/uploads/core/setup.server.ts
@@ -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'
@@ -24,7 +21,15 @@ export async function ensureUploadsDirectory() {
return true
}
+ if (typeof window !== 'undefined' || !UPLOAD_DIR_SERVER) {
+ // Skip on client side
+ return true
+ }
+
try {
+ const { existsSync } = require('fs')
+ const { mkdir } = require('fs/promises')
+
if (!existsSync(UPLOAD_DIR_SERVER)) {
await mkdir(UPLOAD_DIR_SERVER, { recursive: true })
} else {
diff --git a/apps/sim/lib/uploads/core/storage-client.ts b/apps/sim/lib/uploads/core/storage-client.ts
index 2ae29b8a69..60adb6f583 100644
--- a/apps/sim/lib/uploads/core/storage-client.ts
+++ b/apps/sim/lib/uploads/core/storage-client.ts
@@ -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
+
+/**
+ * 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
+
+export async function uploadFile(
+ file: Buffer,
+ fileName: string,
+ contentType: string,
+ configOrSize?: CustomStorageConfig | number,
+ size?: number
+): Promise {
+ 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
+
+/**
+ * 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
+
+export async function downloadFile(
+ key: string,
+ customConfig?: CustomStorageConfig
+): Promise {
+ 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 {
+ 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
diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts
index 628ee264fd..d5630d3822 100644
--- a/apps/sim/next.config.ts
+++ b/apps/sim/next.config.ts
@@ -4,6 +4,30 @@ import { isDev, isHosted } from './lib/environment'
import { getMainCSPPolicy, getWorkflowExecutionCSPPolicy } from './lib/security/csp'
const nextConfig: NextConfig = {
+ webpack: (config, { isServer }) => {
+ if (!isServer) {
+ // Don't resolve server-only modules on the client
+ config.resolve.fallback = {
+ ...config.resolve.fallback,
+ fs: false,
+ 'fs/promises': false,
+ path: false,
+ dns: false,
+ util: false,
+ net: false,
+ tls: false,
+ crypto: false,
+ stream: false,
+ perf_hooks: false,
+ os: false,
+ http: false,
+ https: false,
+ zlib: false,
+ child_process: false,
+ }
+ }
+ return config
+ },
devIndicators: false,
images: {
remotePatterns: [
@@ -75,7 +99,7 @@ const nextConfig: NextConfig = {
turbopack: {
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
},
- serverExternalPackages: ['unpdf', 'ffmpeg-static', 'fluent-ffmpeg'],
+ serverExternalPackages: ['unpdf', 'ffmpeg-static', 'fluent-ffmpeg', '@azure/storage-blob', '@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner'],
experimental: {
optimizeCss: true,
turbopackSourceMaps: false,
diff --git a/apps/sim/package.json b/apps/sim/package.json
index 97d4733aff..d9895b8e54 100644
--- a/apps/sim/package.json
+++ b/apps/sim/package.json
@@ -8,7 +8,7 @@
"node": ">=20.0.0"
},
"scripts": {
- "dev": "next dev --turbo --port 3000",
+ "dev": "next dev --port 3000",
"dev:classic": "next dev",
"dev:sockets": "bun run socket-server/index.ts",
"dev:full": "concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"bun run dev\" \"bun run dev:sockets\"",
@@ -39,6 +39,8 @@
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-node": "^0.200.0",
+ "@opentelemetry/sdk-trace-base": "2.2.0",
+ "@opentelemetry/sdk-trace-node": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.32.0",
"@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-avatar": "1.1.10",
@@ -59,11 +61,13 @@
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-tooltip": "1.2.8",
+ "@radix-ui/react-visually-hidden": "1.2.3",
"@react-email/components": "^0.0.34",
"@trigger.dev/sdk": "4.0.4",
"@types/three": "0.177.0",
"better-auth": "1.3.12",
"browser-image-compression": "^2.0.2",
+ "chalk": "^5.3.0",
"cheerio": "1.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -86,10 +90,10 @@
"js-yaml": "4.1.0",
"jszip": "3.10.1",
"jwt-decode": "^4.0.0",
+ "lodash": "4.17.21",
"lucide-react": "^0.479.0",
"mammoth": "^1.9.0",
"mysql2": "3.14.3",
- "nanoid": "^3.3.7",
"next": "^15.4.1",
"next-mdx-remote": "^5.0.0",
"next-runtime-env": "3.3.0",
@@ -112,6 +116,7 @@
"rehype-slug": "^6.0.0",
"remark-gfm": "4.0.1",
"resend": "^4.1.2",
+ "server-only": "0.0.1",
"sharp": "0.34.3",
"socket.io": "^4.8.1",
"stripe": "18.5.0",
@@ -121,9 +126,11 @@
"unpdf": "1.4.0",
"uuid": "^11.1.0",
"xlsx": "0.18.5",
- "zod": "^3.24.2"
+ "zod": "^3.24.2",
+ "zustand": "^4.5.5"
},
"devDependencies": {
+ "@playwright/test": "1.56.1",
"@testing-library/jest-dom": "^6.6.3",
"@trigger.dev/build": "4.0.4",
"@types/html-to-text": "9.0.4",