From 5fec81320c149b4aa9ff2b6a29b5d0c1d049f367 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 27 Jun 2025 20:36:11 -0700 Subject: [PATCH 1/5] fix sourceBlock null check --- .../[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts index 57e97320c0..2108a0404a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts @@ -167,6 +167,7 @@ export function useBlockConnections(blockId: string) { .filter((edge) => edge.target === blockId) .map((edge) => { const sourceBlock = blocks[edge.source] + if (!sourceBlock) return null // Get the response format from the subblock store instead const responseFormatValue = useSubBlockStore @@ -202,6 +203,7 @@ export function useBlockConnections(blockId: string) { responseFormat, } }) + .filter(Boolean) as ConnectedBlock[] return { incomingConnections: allPathConnections, From e93a5ce64643d10a5be6f751710f7d3db6b89483 Mon Sep 17 00:00:00 2001 From: Aditya Tripathi Date: Sat, 28 Jun 2025 21:06:01 +0530 Subject: [PATCH 2/5] fix(cli): package type for esm imports, missing realtime (#574) * fix: package type for esm imports, missing realtime calls and use of migrate * chore: bump cli --- packages/cli/package.json | 3 ++- packages/cli/src/index.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index db91da2291..8aba88e6d6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,8 +1,9 @@ { "name": "simstudio", - "version": "0.1.18", + "version": "0.1.19", "description": "Sim Studio CLI - Run Sim Studio with a single command", "main": "dist/index.js", + "type": "module", "bin": { "simstudio": "dist/index.js" }, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c40b718804..a2959fad49 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -188,7 +188,7 @@ async function main() { 'ghcr.io/simstudioai/migrations:latest', 'bun', 'run', - 'db:push', + 'db:migrate', ]) if (!migrationsSuccess) { @@ -259,7 +259,7 @@ async function main() { ) console.log( chalk.yellow( - `🛑 To stop all containers, run: ${chalk.bold('docker stop simstudio-app simstudio-db')}` + `🛑 To stop all containers, run: ${chalk.bold('docker stop simstudio-app simstudio-db simstudio-realtime')}` ) ) @@ -275,6 +275,7 @@ async function main() { // Stop containers await stopAndRemoveContainer(APP_CONTAINER) await stopAndRemoveContainer(DB_CONTAINER) + await stopAndRemoveContainer(REALTIME_CONTAINER) console.log(chalk.green('✅ Sim Studio has been stopped')) process.exit(0) From e37a8681e0a36e026d5cdd4e7d0ad0388f3c8559 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 28 Jun 2025 00:14:32 -0400 Subject: [PATCH 3/5] fix sourceBlock null check --- apps/sim/contexts/socket-context.test.tsx | 278 ---------------------- 1 file changed, 278 deletions(-) delete mode 100644 apps/sim/contexts/socket-context.test.tsx diff --git a/apps/sim/contexts/socket-context.test.tsx b/apps/sim/contexts/socket-context.test.tsx deleted file mode 100644 index 5f1c8df744..0000000000 --- a/apps/sim/contexts/socket-context.test.tsx +++ /dev/null @@ -1,278 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import { act, renderHook, waitFor } from '@testing-library/react' -import { io } from 'socket.io-client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { SocketProvider, useSocket } from './socket-context' - -vi.mock('socket.io-client') -const mockIo = vi.mocked(io) - -global.fetch = vi.fn() -const mockFetch = vi.mocked(fetch) - -vi.mock('@/lib/logs/console-logger', () => ({ - createLogger: () => ({ - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - }), -})) - -describe('SocketContext Token Refresh', () => { - let mockSocket: any - let eventHandlers: Record - - beforeEach(() => { - eventHandlers = {} - mockSocket = { - id: 'test-socket-id', - connected: true, - io: { engine: { transport: { name: 'websocket' } } }, - auth: { token: 'initial-token' }, - on: vi.fn((event, handler) => { - eventHandlers[event] = handler - }), - connect: vi.fn(), - disconnect: vi.fn(), - emit: vi.fn(), - close: vi.fn(), - } - - mockIo.mockReturnValue(mockSocket) - - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ token: 'fresh-token' }), - } as Response) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - const renderSocketProvider = async (user = { id: 'test-user', name: 'Test User' }) => { - const result = renderHook(() => useSocket(), { - wrapper: ({ children }) => {children}, - }) - - await waitFor(() => { - expect(mockSocket.on).toHaveBeenCalledWith('connect_error', expect.any(Function)) - }) - - vi.clearAllMocks() - - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ token: 'fresh-token' }), - } as Response) - - return result - } - - describe('Token Refresh on Connection Error', () => { - it('should refresh token on authentication failure', async () => { - const { result } = await renderSocketProvider() - - const error = { message: 'Token validation failed' } - - await act(async () => { - await eventHandlers.connect_error(error) - }) - - expect(mockFetch).toHaveBeenCalledWith('/api/auth/socket-token', { - method: 'POST', - credentials: 'include', - }) - - // Should update socket auth and reconnect - expect(mockSocket.auth.token).toBe('fresh-token') - expect(mockSocket.connect).toHaveBeenCalled() - }) - - it('should limit token refresh attempts to 3', async () => { - const { result } = await renderSocketProvider() - - const error = { message: 'Token validation failed' } - - for (let i = 0; i < 4; i++) { - await act(async () => { - await eventHandlers.connect_error(error) - }) - } - - // Should only call fetch 3 times (max attempts) - expect(mockFetch).toHaveBeenCalledTimes(3) - expect(mockSocket.connect).toHaveBeenCalledTimes(3) - }) - - it('should prevent concurrent token refresh attempts', async () => { - const { result } = await renderSocketProvider() - - let resolveTokenFetch!: (value: { - ok: boolean - json: () => Promise<{ token: string }> - }) => void - const slowTokenPromise = new Promise((resolve) => { - resolveTokenFetch = resolve - }) - - mockFetch.mockReturnValue(slowTokenPromise as any) - - const error = { message: 'Authentication failed' } - - // Start two concurrent refresh attempts - const promise1 = act(async () => { - await eventHandlers.connect_error(error) - }) - - const promise2 = act(async () => { - await eventHandlers.connect_error(error) - }) - - // Resolve the slow fetch - resolveTokenFetch({ - ok: true, - json: async () => ({ token: 'fresh-token' }), - }) - - await Promise.all([promise1, promise2]) - - // Should only call fetch once (concurrent protection) - expect(mockFetch).toHaveBeenCalledTimes(1) - }) - - it('should reset retry counter on successful connection', async () => { - const { result } = await renderSocketProvider() - - const error = { message: 'Token validation failed' } - - // Use up 2 retry attempts - await act(async () => { - await eventHandlers.connect_error(error) - }) - await act(async () => { - await eventHandlers.connect_error(error) - }) - - expect(mockFetch).toHaveBeenCalledTimes(2) - - // Simulate successful connection (resets counter) - await act(async () => { - eventHandlers.connect() - }) - - // Should be able to retry again (counter reset) - await act(async () => { - await eventHandlers.connect_error(error) - }) - - expect(mockFetch).toHaveBeenCalledTimes(3) - }) - - it('should handle token refresh failure gracefully', async () => { - const { result } = await renderSocketProvider() - - // Mock failed token refresh after initialization - mockFetch.mockResolvedValue({ - ok: false, - status: 401, - } as Response) - - const error = { message: 'Token validation failed' } - - await act(async () => { - await eventHandlers.connect_error(error) - }) - - // Should attempt refresh but not update auth or reconnect - expect(mockFetch).toHaveBeenCalled() - expect(mockSocket.auth.token).toBe('initial-token') // unchanged - expect(mockSocket.connect).not.toHaveBeenCalled() - }) - - it('should handle fetch errors gracefully', async () => { - const { result } = await renderSocketProvider() - - // Mock fetch error after initialization - mockFetch.mockRejectedValue(new Error('Network error')) - - const error = { message: 'Authentication failed' } - - // Should not throw error - await act(async () => { - await eventHandlers.connect_error(error) - }) - - expect(mockFetch).toHaveBeenCalled() - expect(mockSocket.connect).not.toHaveBeenCalled() - }) - - it('should only refresh token on authentication-related errors', async () => { - const { result } = await renderSocketProvider() - - // Non-authentication error - const networkError = { message: 'Network timeout' } - - await act(async () => { - await eventHandlers.connect_error(networkError) - }) - - // Should not attempt token refresh - expect(mockFetch).not.toHaveBeenCalled() - expect(mockSocket.connect).not.toHaveBeenCalled() - }) - }) - - describe('Interaction with Socket.IO Reconnection', () => { - it('should work with Socket.IO built-in reconnection attempts', async () => { - const { result } = await renderSocketProvider() - - // Simulate Socket.IO reconnection cycle - await act(async () => { - // Reconnection attempt starts - eventHandlers.reconnect_attempt(1) - }) - - await act(async () => { - // Fails with auth error - await eventHandlers.connect_error({ message: 'Token validation failed' }) - }) - - // Should refresh token and attempt reconnection - expect(mockFetch).toHaveBeenCalled() - expect(mockSocket.connect).toHaveBeenCalled() - }) - - it('should reset counters on successful reconnect', async () => { - const { result } = await renderSocketProvider() - - // Use up retry attempts - const error = { message: 'Authentication failed' } - await act(async () => { - await eventHandlers.connect_error(error) - }) - - await act(async () => { - await eventHandlers.connect_error(error) - }) - - expect(mockFetch).toHaveBeenCalledTimes(2) - - // Simulate successful reconnection - await act(async () => { - eventHandlers.reconnect(1) - }) - - // Should reset and allow new attempts - await act(async () => { - await eventHandlers.connect_error(error) - }) - - expect(mockFetch).toHaveBeenCalledTimes(3) - }) - }) -}) From 2f8d9eaede9638622f27e775d0552549d53b3c63 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Jun 2025 00:38:15 -0400 Subject: [PATCH 4/5] fix(kb): fix kb navigation URLs --- .../[id]/[documentId]/components/document-loading.tsx | 7 +++++-- .../knowledge/[id]/[documentId]/document.tsx | 10 ++++++---- .../workspace/[workspaceId]/knowledge/[id]/base.tsx | 4 ++-- .../knowledge-base-loading/knowledge-base-loading.tsx | 5 ++++- .../components/base-overview/base-overview.tsx | 7 +++++-- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-loading.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-loading.tsx index aad70d0ed4..89f4f7a7f4 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-loading.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-loading.tsx @@ -1,6 +1,7 @@ 'use client' import { Plus, Search } from 'lucide-react' +import { useParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { useSidebarStore } from '@/stores/sidebar/store' import { KnowledgeHeader } from '../../../components/knowledge-header/knowledge-header' @@ -18,6 +19,8 @@ export function DocumentLoading({ documentName, }: DocumentLoadingProps) { const { mode, isExpanded } = useSidebarStore() + const params = useParams() + const workspaceId = params?.workspaceId as string const isSidebarCollapsed = mode === 'expanded' ? !isExpanded : mode === 'collapsed' || mode === 'hover' @@ -25,12 +28,12 @@ export function DocumentLoading({ { id: 'knowledge-root', label: 'Knowledge', - href: '/knowledge', + href: `/workspace/${workspaceId}/knowledge`, }, { id: `knowledge-base-${knowledgeBaseId}`, label: knowledgeBaseName, - href: `/knowledge/${knowledgeBaseId}`, + href: `/workspace/${workspaceId}/knowledge/${knowledgeBaseId}`, }, { id: `document-${knowledgeBaseId}-${documentName}`, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index fc25327f6d..9fe8c73f51 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -12,6 +12,7 @@ import { Trash2, X, } from 'lucide-react' +import { useParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' @@ -54,6 +55,7 @@ export function Document({ }: DocumentProps) { const { mode, isExpanded } = useSidebarStore() const { getCachedKnowledgeBase, getCachedDocuments } = useKnowledgeStore() + const { workspaceId } = useParams() const isSidebarCollapsed = mode === 'expanded' ? !isExpanded : mode === 'collapsed' || mode === 'hover' @@ -170,10 +172,10 @@ export function Document({ const effectiveDocumentName = document?.filename || documentName || 'Document' const breadcrumbs = [ - { label: 'Knowledge', href: '/knowledge' }, + { label: 'Knowledge', href: `/workspace/${workspaceId}/knowledge` }, { label: effectiveKnowledgeBaseName, - href: `/knowledge/${knowledgeBaseId}`, + href: `/workspace/${workspaceId}/knowledge/${knowledgeBaseId}`, }, { label: effectiveDocumentName }, ] @@ -360,10 +362,10 @@ export function Document({ if (combinedError && !isLoadingChunks) { const errorBreadcrumbs = [ - { label: 'Knowledge', href: '/knowledge' }, + { label: 'Knowledge', href: `/workspace/${workspaceId}/knowledge` }, { label: effectiveKnowledgeBaseName, - href: `/knowledge/${knowledgeBaseId}`, + href: `/workspace/${workspaceId}/knowledge/${knowledgeBaseId}`, }, { label: 'Error' }, ] diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index eeffdb79e7..664623b0c8 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -743,7 +743,7 @@ export function KnowledgeBase({ { id: 'knowledge-root', label: 'Knowledge', - href: '/knowledge', + href: `/workspace/${workspaceId}/knowledge`, }, { id: `knowledge-base-${id}`, @@ -762,7 +762,7 @@ export function KnowledgeBase({ { id: 'knowledge-root', label: 'Knowledge', - href: '/knowledge', + href: `/workspace/${workspaceId}/knowledge`, }, { id: 'error', diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx index 150979ddb6..a189d2d193 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/knowledge-base-loading/knowledge-base-loading.tsx @@ -1,6 +1,7 @@ 'use client' import { Search } from 'lucide-react' +import { useParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { useSidebarStore } from '@/stores/sidebar/store' import { KnowledgeHeader } from '../../../components/knowledge-header/knowledge-header' @@ -12,6 +13,8 @@ interface KnowledgeBaseLoadingProps { export function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps) { const { mode, isExpanded } = useSidebarStore() + const params = useParams() + const workspaceId = params?.workspaceId as string const isSidebarCollapsed = mode === 'expanded' ? !isExpanded : mode === 'collapsed' || mode === 'hover' @@ -19,7 +22,7 @@ export function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoading { id: 'knowledge-root', label: 'Knowledge', - href: '/knowledge', + href: `/workspace/${workspaceId}/knowledge`, }, { id: 'knowledge-base-loading', diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-overview/base-overview.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-overview/base-overview.tsx index 223c9e9c90..1ecf717700 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-overview/base-overview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-overview/base-overview.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { Check, Copy, LibraryBig } from 'lucide-react' import Link from 'next/link' +import { useParams } from 'next/navigation' interface BaseOverviewProps { id?: string @@ -13,12 +14,14 @@ interface BaseOverviewProps { export function BaseOverview({ id, title, docCount, description }: BaseOverviewProps) { const [isCopied, setIsCopied] = useState(false) + const params = useParams() + const workspaceId = params?.workspaceId as string // Create URL with knowledge base name as query parameter - const params = new URLSearchParams({ + const searchParams = new URLSearchParams({ kbName: title, }) - const href = `/knowledge/${id || title.toLowerCase().replace(/\s+/g, '-')}?${params.toString()}` + const href = `/workspace/${workspaceId}/knowledge/${id || title.toLowerCase().replace(/\s+/g, '-')}?${searchParams.toString()}` const handleCopy = async (e: React.MouseEvent) => { e.preventDefault() From f08a51cc346d776ea9960472d2cd717c60b22f6e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Jun 2025 11:35:07 -0400 Subject: [PATCH 5/5] fix(csp): update CSP to allow for google drive picker --- apps/sim/next.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index 114b646d04..82ea739586 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -132,7 +132,7 @@ const nextConfig: NextConfig = { }, { // For main app routes, Google Drive Picker, and Vercel resources - use permissive policies - source: '/(w/.*|api/tools/drive|_next/.*|_vercel/.*)', + source: '/(w/.*|workspace/.*|api/tools/drive|_next/.*|_vercel/.*)', headers: [ { key: 'Cross-Origin-Embedder-Policy',