From a05b4422ad7ef0856fedb497ccee976d3c47d54a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Apr 2026 12:22:16 -0700 Subject: [PATCH 1/8] improvement(utils): add shared utility functions and replace inline patterns Add sleep, toError, safeJsonParse, isNonNull helpers and invariant/assertNever assertions. Replace all inline implementations across the codebase with these shared utilities for consistency. Zero behavioral changes. Co-Authored-By: Claude Opus 4.6 --- .claude/rules/global.md | 27 ++++ .cursor/rules/global.mdc | 27 ++++ CLAUDE.md | 1 + .../components/sandbox-canvas-provider.tsx | 3 +- apps/sim/app/api/auth/oauth/utils.ts | 7 +- apps/sim/app/api/auth/socket-token/route.ts | 3 +- apps/sim/app/api/auth/sso/register/route.ts | 26 ++++ apps/sim/app/api/billing/switch-plan/route.ts | 3 +- apps/sim/app/api/billing/update-cost/route.ts | 5 +- apps/sim/app/api/copilot/chat/abort/route.ts | 7 +- apps/sim/app/api/copilot/chat/queries.ts | 7 +- apps/sim/app/api/copilot/chat/stream/route.ts | 11 +- apps/sim/app/api/copilot/confirm/route.ts | 7 +- apps/sim/app/api/copilot/models/route.ts | 3 +- .../cron/cleanup-stale-executions/route.ts | 9 +- apps/sim/app/api/jobs/[jobId]/route.ts | 3 +- apps/sim/app/api/mcp/copilot/route.ts | 15 ++- .../app/api/mcp/servers/[id]/refresh/route.ts | 7 +- apps/sim/app/api/mcp/servers/[id]/route.ts | 7 +- apps/sim/app/api/mcp/servers/route.ts | 19 +-- .../api/mcp/servers/test-connection/route.ts | 7 +- apps/sim/app/api/mcp/tools/stored/route.ts | 7 +- .../api/mcp/workflow-servers/[id]/route.ts | 19 +-- .../[id]/tools/[toolId]/route.ts | 19 +-- .../mcp/workflow-servers/[id]/tools/route.ts | 13 +- .../sim/app/api/mcp/workflow-servers/route.ts | 13 +- .../api/mothership/chats/[chatId]/route.ts | 7 +- apps/sim/app/api/mothership/execute/route.ts | 3 +- apps/sim/app/api/providers/route.ts | 10 +- .../[executionId]/[contextId]/route.ts | 3 +- apps/sim/app/api/schedules/execute/route.ts | 5 +- .../api/table/[tableId]/import-csv/route.ts | 7 +- .../api/table/[tableId]/rows/[rowId]/route.ts | 5 +- .../sim/app/api/table/[tableId]/rows/route.ts | 11 +- .../api/table/[tableId]/rows/upsert/route.ts | 3 +- apps/sim/app/api/table/import-csv/route.ts | 3 +- .../api/templates/approved/sanitized/route.ts | 5 +- .../app/api/tools/a2a/send-message/route.ts | 3 +- .../sim/app/api/tools/agiloft/attach/route.ts | 23 ++-- .../app/api/tools/asana/create-task/route.ts | 3 +- .../app/api/tools/asana/update-task/route.ts | 3 +- apps/sim/app/api/tools/cloudwatch/utils.ts | 3 +- apps/sim/app/api/tools/image/route.ts | 3 +- apps/sim/app/api/tools/jira/update/route.ts | 3 +- apps/sim/app/api/tools/jira/write/route.ts | 3 +- apps/sim/app/api/tools/jsm/approvals/route.ts | 3 +- apps/sim/app/api/tools/jsm/comment/route.ts | 3 +- apps/sim/app/api/tools/jsm/comments/route.ts | 3 +- apps/sim/app/api/tools/jsm/customers/route.ts | 3 +- .../app/api/tools/jsm/forms/answers/route.ts | 3 +- .../app/api/tools/jsm/forms/attach/route.ts | 3 +- .../sim/app/api/tools/jsm/forms/copy/route.ts | 3 +- .../app/api/tools/jsm/forms/delete/route.ts | 3 +- .../api/tools/jsm/forms/externalise/route.ts | 3 +- apps/sim/app/api/tools/jsm/forms/get/route.ts | 3 +- .../api/tools/jsm/forms/internalise/route.ts | 3 +- .../app/api/tools/jsm/forms/issue/route.ts | 3 +- .../app/api/tools/jsm/forms/reopen/route.ts | 3 +- .../sim/app/api/tools/jsm/forms/save/route.ts | 3 +- .../api/tools/jsm/forms/structure/route.ts | 3 +- .../app/api/tools/jsm/forms/submit/route.ts | 3 +- .../api/tools/jsm/forms/templates/route.ts | 3 +- .../app/api/tools/jsm/organization/route.ts | 3 +- .../app/api/tools/jsm/organizations/route.ts | 3 +- .../app/api/tools/jsm/participants/route.ts | 3 +- apps/sim/app/api/tools/jsm/queues/route.ts | 3 +- apps/sim/app/api/tools/jsm/request/route.ts | 3 +- apps/sim/app/api/tools/jsm/requests/route.ts | 3 +- .../api/tools/jsm/requesttypefields/route.ts | 3 +- .../app/api/tools/jsm/requesttypes/route.ts | 3 +- .../app/api/tools/jsm/servicedesks/route.ts | 3 +- apps/sim/app/api/tools/jsm/sla/route.ts | 3 +- .../sim/app/api/tools/jsm/transition/route.ts | 3 +- .../app/api/tools/jsm/transitions/route.ts | 3 +- .../tools/microsoft-teams/channels/route.ts | 3 +- .../api/tools/microsoft-teams/chats/route.ts | 9 +- .../api/tools/microsoft-teams/teams/route.ts | 3 +- apps/sim/app/api/tools/onepassword/utils.ts | 3 +- .../app/api/tools/outlook/folders/route.ts | 3 +- apps/sim/app/api/tools/sftp/utils.ts | 3 +- apps/sim/app/api/tools/smtp/send/route.ts | 3 +- apps/sim/app/api/tools/ssh/utils.ts | 3 +- apps/sim/app/api/tools/stt/route.ts | 3 +- .../tools/supabase/storage-upload/route.ts | 15 ++- .../sim/app/api/tools/textract/parse/route.ts | 5 +- apps/sim/app/api/tools/video/route.ts | 5 +- .../me/subscription/[id]/transfer/route.ts | 3 +- apps/sim/app/api/users/me/usage-logs/route.ts | 3 +- apps/sim/app/api/v1/copilot/chat/route.ts | 3 +- .../v1/tables/[tableId]/rows/[rowId]/route.ts | 3 +- .../app/api/v1/tables/[tableId]/rows/route.ts | 9 +- .../v1/tables/[tableId]/rows/upsert/route.ts | 3 +- .../app/api/workflows/[id]/execute/route.ts | 10 +- .../executions/[executionId]/stream/route.ts | 5 +- .../sim/app/api/workflows/[id]/state/route.ts | 3 +- .../[notificationId]/test/route.ts | 5 +- .../message/components/file-download.tsx | 3 +- apps/sim/app/ingest/[[...path]]/route.ts | 3 +- .../[workspaceId]/home/hooks/use-chat.ts | 7 +- .../knowledge/hooks/use-knowledge-upload.ts | 6 +- .../components/usage-limit/usage-limit.tsx | 3 +- .../w/[workflowId]/components/panel/panel.tsx | 3 +- .../hooks/use-workflow-execution.ts | 5 +- apps/sim/background/resume-execution.ts | 3 +- apps/sim/background/schedule-execution.ts | 9 +- apps/sim/background/webhook-execution.ts | 5 +- apps/sim/background/workflow-execution.ts | 5 +- .../workspace-notification-delivery.ts | 3 +- apps/sim/blocks/blocks/langsmith.ts | 5 +- apps/sim/blocks/blocks/mistral_parse.ts | 7 +- apps/sim/blocks/blocks/notion.ts | 13 +- apps/sim/blocks/blocks/reducto.ts | 5 +- apps/sim/blocks/blocks/sharepoint.ts | 3 +- apps/sim/blocks/blocks/table.ts | 3 +- apps/sim/blocks/utils.ts | 5 +- apps/sim/connectors/airtable/airtable.ts | 3 +- apps/sim/connectors/asana/asana.ts | 3 +- apps/sim/connectors/confluence/confluence.ts | 3 +- apps/sim/connectors/discord/discord.ts | 3 +- apps/sim/connectors/dropbox/dropbox.ts | 3 +- apps/sim/connectors/evernote/evernote.ts | 3 +- apps/sim/connectors/fireflies/fireflies.ts | 3 +- apps/sim/connectors/github/github.ts | 3 +- apps/sim/connectors/gmail/gmail.ts | 3 +- .../sim/connectors/google-docs/google-docs.ts | 3 +- .../connectors/google-drive/google-drive.ts | 3 +- .../connectors/google-sheets/google-sheets.ts | 7 +- apps/sim/connectors/intercom/intercom.ts | 3 +- apps/sim/connectors/linear/linear.ts | 3 +- .../microsoft-teams/microsoft-teams.ts | 3 +- apps/sim/connectors/notion/notion.ts | 5 +- apps/sim/connectors/obsidian/obsidian.ts | 5 +- apps/sim/connectors/onedrive/onedrive.ts | 3 +- apps/sim/connectors/outlook/outlook.ts | 3 +- apps/sim/connectors/reddit/reddit.ts | 5 +- apps/sim/connectors/servicenow/servicenow.ts | 3 +- apps/sim/connectors/sharepoint/sharepoint.ts | 3 +- apps/sim/connectors/slack/slack.ts | 5 +- apps/sim/connectors/webflow/webflow.ts | 3 +- apps/sim/connectors/wordpress/wordpress.ts | 3 +- apps/sim/connectors/zendesk/zendesk.ts | 3 +- apps/sim/executor/dag/construction/edges.ts | 5 +- apps/sim/executor/execution/block-executor.ts | 5 +- apps/sim/executor/execution/engine.ts | 5 +- .../executor/handlers/agent/agent-handler.ts | 3 +- .../handlers/generic/generic-handler.ts | 3 +- .../handlers/mothership/mothership-handler.ts | 3 +- apps/sim/executor/orchestrators/loop.ts | 3 +- apps/sim/executor/orchestrators/parallel.ts | 3 +- .../sim/executor/utils/file-tool-processor.ts | 3 +- apps/sim/executor/utils/subflow-utils.ts | 19 ++- apps/sim/executor/variables/resolver.ts | 5 +- apps/sim/lib/a2a/utils.ts | 3 +- apps/sim/lib/auth/auth.ts | 7 +- apps/sim/lib/auth/cimd.ts | 3 +- .../lib/billing/calculations/usage-monitor.ts | 5 +- apps/sim/lib/billing/core/usage-log.ts | 3 +- apps/sim/lib/chunkers/regex-chunker.ts | 5 +- apps/sim/lib/copilot/chat/payload.ts | 11 +- apps/sim/lib/copilot/chat/post.ts | 3 +- .../sim/lib/copilot/chat/workspace-context.ts | 3 +- .../copilot/persistence/tool-confirm/index.ts | 5 +- .../request/go/file-preview-adapter.ts | 3 +- apps/sim/lib/copilot/request/go/parser.ts | 9 +- apps/sim/lib/copilot/request/go/stream.ts | 3 +- apps/sim/lib/copilot/request/handlers/tool.ts | 7 +- .../sim/lib/copilot/request/handlers/types.ts | 3 +- .../lib/copilot/request/lifecycle/finalize.ts | 3 +- .../lib/copilot/request/lifecycle/headless.ts | 3 +- apps/sim/lib/copilot/request/lifecycle/run.ts | 7 +- .../lib/copilot/request/lifecycle/start.ts | 5 +- apps/sim/lib/copilot/request/session/abort.ts | 17 +-- .../sim/lib/copilot/request/session/buffer.ts | 7 +- .../request/session/file-preview-session.ts | 9 +- .../sim/lib/copilot/request/session/writer.ts | 9 +- apps/sim/lib/copilot/request/subagent.ts | 3 +- .../sim/lib/copilot/request/tools/executor.ts | 23 ++-- apps/sim/lib/copilot/request/tools/files.ts | 3 +- .../lib/copilot/request/tools/resources.ts | 5 +- apps/sim/lib/copilot/request/tools/tables.ts | 9 +- apps/sim/lib/copilot/resources/persistence.ts | 5 +- .../sim/lib/copilot/tool-executor/executor.ts | 3 +- .../tools/client/run-tool-execution.ts | 7 +- .../tools/handlers/deployment/deploy.ts | 9 +- .../tools/handlers/deployment/manage.ts | 15 ++- apps/sim/lib/copilot/tools/handlers/jobs.ts | 17 +-- .../handlers/management/manage-credential.ts | 3 +- .../handlers/management/manage-custom-tool.ts | 3 +- .../handlers/management/manage-mcp-tool.ts | 3 +- .../tools/handlers/management/manage-skill.ts | 3 +- .../tools/handlers/materialize-file.ts | 3 +- apps/sim/lib/copilot/tools/handlers/oauth.ts | 5 +- .../tools/handlers/restore-resource.ts | 3 +- .../tools/handlers/upload-file-reader.ts | 5 +- apps/sim/lib/copilot/tools/handlers/vfs.ts | 9 +- .../tools/handlers/workflow/mutations.ts | 29 +++-- .../tools/handlers/workflow/queries.ts | 13 +- .../tools/registry/server-tool-adapter.ts | 3 +- .../server/blocks/get-blocks-metadata-tool.ts | 5 +- .../tools/server/files/edit-content.ts | 3 +- .../tools/server/files/file-intent-store.ts | 7 +- .../tools/server/files/file-preview.ts | 3 +- .../tools/server/files/workspace-file.ts | 3 +- .../tools/server/image/generate-image.ts | 3 +- .../tools/server/knowledge/knowledge-base.ts | 3 +- .../tools/server/other/search-online.ts | 3 +- .../copilot/tools/server/table/user-table.ts | 10 +- .../tools/server/user/get-credentials.ts | 5 +- .../server/workflow/edit-workflow/index.ts | 5 +- .../copilot/validation/selector-validator.ts | 3 +- apps/sim/lib/copilot/vfs/file-reader.ts | 5 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 35 ++--- apps/sim/lib/core/config/redis.ts | 3 +- apps/sim/lib/core/idempotency/cleanup.ts | 3 +- apps/sim/lib/core/idempotency/service.ts | 3 +- .../sim/lib/core/rate-limiter/rate-limiter.ts | 7 +- .../core/security/input-validation.server.ts | 5 +- .../core/security/input-validation.test.ts | 120 ++++++++++++++++++ .../sim/lib/core/security/input-validation.ts | 49 +++++++ apps/sim/lib/core/telemetry.ts | 3 +- apps/sim/lib/core/utils/asserts.ts | 17 +++ apps/sim/lib/core/utils/helpers.ts | 39 ++++++ apps/sim/lib/execution/buffered-stream.ts | 5 +- apps/sim/lib/execution/doc-vm.ts | 5 +- apps/sim/lib/execution/event-buffer.ts | 9 +- apps/sim/lib/execution/isolated-vm.ts | 3 +- apps/sim/lib/execution/pptx-vm.ts | 5 +- .../lib/knowledge/connectors/sync-engine.ts | 17 +-- .../knowledge/documents/document-processor.ts | 11 +- apps/sim/lib/knowledge/documents/service.ts | 3 +- apps/sim/lib/knowledge/documents/utils.ts | 7 +- .../sim/lib/logs/execution/logging-session.ts | 29 +++-- apps/sim/lib/mcp/domain-check.ts | 3 +- apps/sim/lib/mcp/middleware.ts | 9 +- apps/sim/lib/mcp/service.ts | 7 +- apps/sim/lib/oauth/oauth.ts | 3 +- apps/sim/lib/og/capture-preview.ts | 3 +- apps/sim/lib/tokenization/calculators.ts | 5 +- apps/sim/lib/tokenization/streaming.ts | 3 +- apps/sim/lib/tokenization/utils.ts | 5 +- apps/sim/lib/webhooks/pending-verification.ts | 3 +- apps/sim/lib/webhooks/processor.ts | 9 +- .../lib/webhooks/provider-subscriptions.ts | 3 +- apps/sim/lib/webhooks/providers/attio.ts | 3 +- apps/sim/lib/webhooks/providers/gong.ts | 3 +- apps/sim/lib/webhooks/providers/linear.ts | 5 +- .../lib/webhooks/providers/microsoft-teams.ts | 3 +- apps/sim/lib/webhooks/providers/monday.ts | 7 +- apps/sim/lib/webhooks/providers/slack.ts | 7 +- apps/sim/lib/webhooks/providers/zoom.ts | 3 +- apps/sim/lib/workflows/diff/diff-engine.ts | 9 +- .../workflows/executor/execute-workflow.ts | 3 +- .../executor/human-in-the-loop-manager.ts | 11 +- .../workflows/executor/pause-persistence.ts | 7 +- .../executor/queued-workflow-execution.ts | 11 +- .../persistence/custom-tools-persistence.ts | 3 +- .../lib/workflows/sanitization/validation.ts | 5 +- apps/sim/lib/workflows/schedules/utils.ts | 7 +- .../lib/workflows/triggers/trigger-utils.ts | 3 +- apps/sim/providers/anthropic/core.ts | 5 +- apps/sim/providers/azure-openai/index.ts | 3 +- apps/sim/providers/bedrock/index.ts | 3 +- apps/sim/providers/cerebras/index.ts | 5 +- apps/sim/providers/deepseek/index.ts | 3 +- apps/sim/providers/fireworks/index.ts | 7 +- apps/sim/providers/gemini/core.ts | 13 +- apps/sim/providers/google/utils.ts | 3 +- apps/sim/providers/groq/index.ts | 3 +- apps/sim/providers/index.ts | 3 +- apps/sim/providers/mistral/index.ts | 3 +- apps/sim/providers/ollama/index.ts | 3 +- apps/sim/providers/openai/core.ts | 3 +- apps/sim/providers/openrouter/index.ts | 7 +- apps/sim/providers/openrouter/utils.ts | 3 +- apps/sim/providers/vllm/index.ts | 3 +- apps/sim/providers/xai/index.ts | 9 +- apps/sim/serializer/index.ts | 3 +- apps/sim/socket/middleware/auth.ts | 3 +- apps/sim/stores/workflow-diff/store.ts | 5 +- apps/sim/stores/workflows/workflow/store.ts | 3 +- apps/sim/tools/agiloft/attachment_info.ts | 12 +- apps/sim/tools/agiloft/create_record.ts | 4 +- apps/sim/tools/agiloft/lock_record.ts | 14 +- apps/sim/tools/agiloft/read_record.ts | 4 +- apps/sim/tools/agiloft/saved_search.ts | 14 +- apps/sim/tools/agiloft/search_records.ts | 16 ++- apps/sim/tools/agiloft/select_records.ts | 6 +- apps/sim/tools/agiloft/update_record.ts | 4 +- apps/sim/tools/agiloft/utils.ts | 72 +++++++---- apps/sim/tools/apify/run_actor_async.ts | 3 +- apps/sim/tools/brightdata/discover.ts | 7 +- apps/sim/tools/browser_use/run_task.ts | 5 +- apps/sim/tools/exa/research.ts | 3 +- apps/sim/tools/extend/parser.ts | 3 +- apps/sim/tools/firecrawl/agent.ts | 3 +- apps/sim/tools/firecrawl/crawl.ts | 3 +- apps/sim/tools/firecrawl/extract.ts | 3 +- apps/sim/tools/index.ts | 24 ++-- apps/sim/tools/mistral/parser.ts | 11 +- apps/sim/tools/notion/query_database.ts | 9 +- apps/sim/tools/pulse/parser.ts | 7 +- apps/sim/tools/reducto/parser.ts | 3 +- apps/sim/tools/sharepoint/create_list.ts | 3 +- apps/sim/tools/sharepoint/read_page.ts | 3 +- apps/sim/tools/supabase/count.ts | 3 +- apps/sim/tools/supabase/delete.ts | 3 +- apps/sim/tools/supabase/get_row.ts | 3 +- apps/sim/tools/supabase/insert.ts | 3 +- apps/sim/tools/supabase/introspect.ts | 8 +- apps/sim/tools/supabase/query.ts | 3 +- apps/sim/tools/supabase/rpc.ts | 3 +- apps/sim/tools/supabase/storage_copy.ts | 3 +- .../tools/supabase/storage_create_bucket.ts | 3 +- .../supabase/storage_create_signed_url.ts | 8 +- apps/sim/tools/supabase/storage_delete.ts | 3 +- .../tools/supabase/storage_delete_bucket.ts | 3 +- apps/sim/tools/supabase/storage_download.ts | 3 +- .../tools/supabase/storage_get_public_url.ts | 8 +- apps/sim/tools/supabase/storage_list.ts | 3 +- .../tools/supabase/storage_list_buckets.ts | 3 +- apps/sim/tools/supabase/storage_move.ts | 3 +- apps/sim/tools/supabase/text_search.ts | 3 +- apps/sim/tools/supabase/update.ts | 3 +- apps/sim/tools/supabase/upsert.ts | 3 +- apps/sim/tools/supabase/utils.test.ts | 41 ++++++ apps/sim/tools/supabase/utils.ts | 14 ++ apps/sim/tools/supabase/vector_search.ts | 3 +- apps/sim/tools/textract/parser.ts | 5 +- apps/sim/tools/tinybird/query.ts | 3 +- 329 files changed, 1389 insertions(+), 790 deletions(-) create mode 100644 apps/sim/lib/core/utils/asserts.ts create mode 100644 apps/sim/lib/core/utils/helpers.ts create mode 100644 apps/sim/tools/supabase/utils.test.ts create mode 100644 apps/sim/tools/supabase/utils.ts diff --git a/.claude/rules/global.md b/.claude/rules/global.md index b80c3695ce3..85877cca174 100644 --- a/.claude/rules/global.md +++ b/.claude/rules/global.md @@ -30,5 +30,32 @@ const shortId = generateShortId() const tiny = generateShortId(8) ``` +## Common Utilities +Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations: + +- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))` +- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))` +- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)` +- `safeJsonParse(str, fallback?)` — parse JSON without throwing. Never write `try { JSON.parse(str) } catch { return default }` +- `isNonNull(value)` — type-narrowing filter predicate for null/undefined + +Use assertion utilities from `@/lib/core/utils/asserts`: + +- `invariant(condition, message)` — assert a condition is truthy, throws if not +- `assertNever(value)` — exhaustive switch/if-else check, TypeScript errors at compile time if a case is unhandled + +```typescript +// ✗ Bad +await new Promise(resolve => setTimeout(resolve, 1000)) +const msg = error instanceof Error ? error.message : String(error) +const err = error instanceof Error ? error : new Error(String(error)) + +// ✓ Good +import { sleep, toError } from '@/lib/core/utils/helpers' +await sleep(1000) +const msg = toError(error).message +const err = toError(error) +``` + ## Package Manager Use `bun` and `bunx`, not `npm` and `npx`. diff --git a/.cursor/rules/global.mdc b/.cursor/rules/global.mdc index af32f057955..f031fdc8b62 100644 --- a/.cursor/rules/global.mdc +++ b/.cursor/rules/global.mdc @@ -37,5 +37,32 @@ const shortId = generateShortId() const tiny = generateShortId(8) ``` +## Common Utilities +Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations: + +- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))` +- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))` +- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)` +- `safeJsonParse(str, fallback?)` — parse JSON without throwing. Never write `try { JSON.parse(str) } catch { return default }` +- `isNonNull(value)` — type-narrowing filter predicate for null/undefined + +Use assertion utilities from `@/lib/core/utils/asserts`: + +- `invariant(condition, message)` — assert a condition is truthy, throws if not +- `assertNever(value)` — exhaustive switch/if-else check, TypeScript errors at compile time if a case is unhandled + +```typescript +// ✗ Bad +await new Promise(resolve => setTimeout(resolve, 1000)) +const msg = error instanceof Error ? error.message : String(error) +const err = error instanceof Error ? error : new Error(String(error)) + +// ✓ Good +import { sleep, toError } from '@/lib/core/utils/helpers' +await sleep(1000) +const msg = toError(error).message +const err = toError(error) +``` + ## Package Manager Use `bun` and `bunx`, not `npm` and `npx`. diff --git a/CLAUDE.md b/CLAUDE.md index bc54c6f912c..e130c6c9966 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,7 @@ You are a professional software engineer. All code must follow best practices: a - **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments - **Styling**: Never update global styles. Keep all styling local to components - **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid` +- **Common Utilities**: Use shared helpers from `@/lib/core/utils/helpers` instead of inline implementations. `sleep(ms)` for delays, `toError(e)` to normalize caught values, `safeJsonParse(str, fallback?)` for safe JSON parsing, `isNonNull(v)` for type-narrowing null filters. Use `invariant(cond, msg)` and `assertNever(val)` from `@/lib/core/utils/asserts` for runtime assertions and exhaustive checks. - **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx` ## Architecture diff --git a/apps/sim/app/academy/components/sandbox-canvas-provider.tsx b/apps/sim/app/academy/components/sandbox-canvas-provider.tsx index 3bd682f43ef..893bb1eaf03 100644 --- a/apps/sim/app/academy/components/sandbox-canvas-provider.tsx +++ b/apps/sim/app/academy/components/sandbox-canvas-provider.tsx @@ -12,6 +12,7 @@ import type { } from '@/lib/academy/types' import { validateExercise } from '@/lib/academy/validation' import { cn } from '@/lib/core/utils/cn' +import { sleep } from '@/lib/core/utils/helpers' import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' @@ -323,7 +324,7 @@ export function SandboxCanvasProvider({ for (let i = 0; i < plan.length; i++) { const step = plan[i] setActiveBlocks(workflowId, new Set([step.blockId])) - await new Promise((resolve) => setTimeout(resolve, step.delay)) + await sleep(step.delay) addConsole({ workflowId, blockId: step.blockId, diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index 0f14e9b0ac9..920c969b939 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -4,6 +4,7 @@ import { account, credential, credentialSetMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, desc, eq, inArray } from 'drizzle-orm' import { decryptSecret } from '@/lib/core/security/encryption' +import { toError } from '@/lib/core/utils/helpers' import { refreshOAuthToken } from '@/lib/oauth' import { getMicrosoftRefreshTokenExpiry, @@ -331,7 +332,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise return accessToken } catch (error) { logger.error(`Error refreshing token for user ${userId}, provider ${providerId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, providerId, userId, @@ -460,7 +461,7 @@ export async function refreshAccessTokenIfNeeded( return refreshedToken.accessToken } catch (error) { logger.error(`[${requestId}] Error refreshing token for credential`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, providerId: credential.providerId, credentialId, @@ -664,7 +665,7 @@ export async function getCredentialsForCredentialSet( } } catch (error) { logger.error(`Failed to refresh token for user ${cred.userId}, provider ${providerId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) continue } diff --git a/apps/sim/app/api/auth/socket-token/route.ts b/apps/sim/app/api/auth/socket-token/route.ts index 810f149b8bb..0228fe58c30 100644 --- a/apps/sim/app/api/auth/socket-token/route.ts +++ b/apps/sim/app/api/auth/socket-token/route.ts @@ -3,6 +3,7 @@ import { headers } from 'next/headers' import { NextResponse } from 'next/server' import { auth } from '@/lib/auth' import { isAuthDisabled } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('SocketTokenAPI') @@ -36,7 +37,7 @@ export async function POST() { } logger.error('Failed to generate socket token', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 }) diff --git a/apps/sim/app/api/auth/sso/register/route.ts b/apps/sim/app/api/auth/sso/register/route.ts index 94c57c93478..809a921655c 100644 --- a/apps/sim/app/api/auth/sso/register/route.ts +++ b/apps/sim/app/api/auth/sso/register/route.ts @@ -147,6 +147,32 @@ export async function POST(request: NextRequest) { oidcConfig.userInfoEndpoint = userInfoEndpoint oidcConfig.jwksEndpoint = jwksEndpoint + const userProvidedEndpoints: Record = { + authorizationEndpoint, + tokenEndpoint, + userInfoEndpoint, + jwksEndpoint, + } + + for (const [name, endpointUrl] of Object.entries(userProvidedEndpoints)) { + if (endpointUrl) { + const endpointValidation = await validateUrlWithDNS(endpointUrl, `OIDC ${name}`) + if (!endpointValidation.isValid) { + logger.warn('Explicitly provided OIDC endpoint failed SSRF validation', { + endpoint: name, + url: endpointUrl, + error: endpointValidation.error, + }) + return NextResponse.json( + { + error: `OIDC ${name} failed security validation: ${endpointValidation.error}`, + }, + { status: 400 } + ) + } + } + } + const needsDiscovery = !oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint diff --git a/apps/sim/app/api/billing/switch-plan/route.ts b/apps/sim/app/api/billing/switch-plan/route.ts index 4bb7dbb366c..cdc1ca5e65c 100644 --- a/apps/sim/app/api/billing/switch-plan/route.ts +++ b/apps/sim/app/api/billing/switch-plan/route.ts @@ -17,6 +17,7 @@ import { hasUsableSubscriptionStatus, } from '@/lib/billing/subscriptions/utils' import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import { captureServerEvent } from '@/lib/posthog/server' const logger = createLogger('SwitchPlan') @@ -185,7 +186,7 @@ export async function POST(request: NextRequest) { } catch (error) { logger.error('Failed to switch subscription', { userId: session?.user?.id, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return NextResponse.json( { error: error instanceof Error ? error.message : 'Failed to switch plan' }, diff --git a/apps/sim/app/api/billing/update-cost/route.ts b/apps/sim/app/api/billing/update-cost/route.ts index f01ec13f939..733be3edd6a 100644 --- a/apps/sim/app/api/billing/update-cost/route.ts +++ b/apps/sim/app/api/billing/update-cost/route.ts @@ -7,6 +7,7 @@ import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing' import { checkInternalApiKey } from '@/lib/copilot/request/http' import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { type AtomicClaimResult, billingIdempotency } from '@/lib/core/idempotency/service' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' const logger = createLogger('BillingUpdateCostAPI') @@ -170,7 +171,7 @@ export async function POST(req: NextRequest) { const duration = Date.now() - startTime logger.error(`[${requestId}] Cost update failed`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, duration, }) @@ -180,7 +181,7 @@ export async function POST(req: NextRequest) { .release(claim.normalizedKey, claim.storageMethod) .catch((releaseErr) => { logger.warn(`[${requestId}] Failed to release idempotency claim`, { - error: releaseErr instanceof Error ? releaseErr.message : String(releaseErr), + error: toError(releaseErr).message, normalizedKey: claim?.normalizedKey, }) }) diff --git a/apps/sim/app/api/copilot/chat/abort/route.ts b/apps/sim/app/api/copilot/chat/abort/route.ts index 375065eb418..f49168b8820 100644 --- a/apps/sim/app/api/copilot/chat/abort/route.ts +++ b/apps/sim/app/api/copilot/chat/abort/route.ts @@ -5,6 +5,7 @@ import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http' import { abortActiveStream, waitForPendingChatStream } from '@/lib/copilot/request/session' import { env } from '@/lib/core/config/env' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('CopilotChatAbortAPI') const GO_EXPLICIT_ABORT_TIMEOUT_MS = 3000 @@ -20,7 +21,7 @@ export async function POST(request: Request) { const body = await request.json().catch((err) => { logger.warn('Abort request body parse failed; continuing with empty object', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return {} }) @@ -35,7 +36,7 @@ export async function POST(request: Request) { const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => { logger.warn('getLatestRunForStream failed while resolving chatId for abort', { streamId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return null }) @@ -70,7 +71,7 @@ export async function POST(request: Request) { } catch (err) { logger.warn('Explicit abort marker request failed; proceeding with local abort', { streamId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) } diff --git a/apps/sim/app/api/copilot/chat/queries.ts b/apps/sim/app/api/copilot/chat/queries.ts index 9977f5419b1..25274224be8 100644 --- a/apps/sim/app/api/copilot/chat/queries.ts +++ b/apps/sim/app/api/copilot/chat/queries.ts @@ -16,6 +16,7 @@ import { import { readFilePreviewSessions } from '@/lib/copilot/request/session' import { readEvents } from '@/lib/copilot/request/session/buffer' import { toStreamBatchEvent } from '@/lib/copilot/request/session/types' +import { toError } from '@/lib/core/utils/helpers' import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils' @@ -82,7 +83,7 @@ export async function GET(req: NextRequest) { logger.warn('Failed to read preview sessions for copilot chat', { chatId, conversationId: chat.conversationId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return [] }), @@ -90,7 +91,7 @@ export async function GET(req: NextRequest) { logger.warn('Failed to fetch latest run for copilot chat snapshot', { chatId, conversationId: chat.conversationId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null }), @@ -110,7 +111,7 @@ export async function GET(req: NextRequest) { logger.warn('Failed to load copilot chat stream snapshot', { chatId, conversationId: chat.conversationId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/app/api/copilot/chat/stream/route.ts b/apps/sim/app/api/copilot/chat/stream/route.ts index 5028ecf7e5e..a0965b189e2 100644 --- a/apps/sim/app/api/copilot/chat/stream/route.ts +++ b/apps/sim/app/api/copilot/chat/stream/route.ts @@ -15,6 +15,7 @@ import { SSE_RESPONSE_HEADERS, } from '@/lib/copilot/request/session' import { toStreamBatchEvent } from '@/lib/copilot/request/session/types' +import { sleep, toError } from '@/lib/core/utils/helpers' export const maxDuration = 3600 @@ -97,7 +98,7 @@ export async function GET(request: NextRequest) { const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => { logger.warn('Failed to fetch latest run for stream', { streamId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return null }) @@ -119,7 +120,7 @@ export async function GET(request: NextRequest) { readFilePreviewSessions(streamId).catch((error) => { logger.warn('Failed to read preview sessions for stream batch', { streamId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return [] }), @@ -235,7 +236,7 @@ export async function GET(request: NextRequest) { (err) => { logger.warn('Failed to poll latest run for stream', { streamId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return null } @@ -273,7 +274,7 @@ export async function GET(request: NextRequest) { break } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) } if (!controllerClosed && Date.now() - startTime >= MAX_STREAM_MS) { emitTerminalIfMissing(MothershipStreamV1CompletionStatus.error, { @@ -286,7 +287,7 @@ export async function GET(request: NextRequest) { if (!controllerClosed && !request.signal.aborted) { logger.warn('Stream replay failed', { streamId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) emitTerminalIfMissing(MothershipStreamV1CompletionStatus.error, { message: 'The stream replay failed before completion.', diff --git a/apps/sim/app/api/copilot/confirm/route.ts b/apps/sim/app/api/copilot/confirm/route.ts index 83aea100f6b..ac1669d7c33 100644 --- a/apps/sim/app/api/copilot/confirm/route.ts +++ b/apps/sim/app/api/copilot/confirm/route.ts @@ -22,6 +22,7 @@ import { createRequestTracker, createUnauthorizedResponse, } from '@/lib/copilot/request/http' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('CopilotConfirmAPI') @@ -106,7 +107,7 @@ async function updateToolCallStatus( logger.error('Failed to update tool call status', { toolCallId, status, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return false } @@ -133,7 +134,7 @@ export async function POST(req: NextRequest) { const existing = await getAsyncToolCall(toolCallId).catch((err) => { logger.warn('Failed to fetch async tool call', { toolCallId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return null }) @@ -145,7 +146,7 @@ export async function POST(req: NextRequest) { const run = await getRunSegment(existing.runId).catch((err) => { logger.warn('Failed to fetch run segment', { runId: existing.runId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return null }) diff --git a/apps/sim/app/api/copilot/models/route.ts b/apps/sim/app/api/copilot/models/route.ts index 7e23e38df69..5aaa7a44a75 100644 --- a/apps/sim/app/api/copilot/models/route.ts +++ b/apps/sim/app/api/copilot/models/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http' +import { toError } from '@/lib/core/utils/helpers' interface AvailableModel { id: string @@ -76,7 +77,7 @@ export async function GET(_req: NextRequest) { return NextResponse.json({ success: true, models }) } catch (error) { logger.error('Error fetching available models', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return NextResponse.json( { diff --git a/apps/sim/app/api/cron/cleanup-stale-executions/route.ts b/apps/sim/app/api/cron/cleanup-stale-executions/route.ts index 175654ca69f..2b52a3ea43e 100644 --- a/apps/sim/app/api/cron/cleanup-stale-executions/route.ts +++ b/apps/sim/app/api/cron/cleanup-stale-executions/route.ts @@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' import { JOB_RETENTION_HOURS, JOB_STATUS } from '@/lib/core/async-jobs' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('CleanupStaleExecutions') @@ -73,7 +74,7 @@ export async function GET(request: NextRequest) { cleaned++ } catch (error) { logger.error(`Failed to clean up execution ${execution.executionId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) failed++ } @@ -104,7 +105,7 @@ export async function GET(request: NextRequest) { } } catch (error) { logger.error('Failed to clean up stale async jobs:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } @@ -131,7 +132,7 @@ export async function GET(request: NextRequest) { } } catch (error) { logger.error('Failed to clean up stale pending jobs:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } @@ -158,7 +159,7 @@ export async function GET(request: NextRequest) { } } catch (error) { logger.error('Failed to delete old async jobs:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } diff --git a/apps/sim/app/api/jobs/[jobId]/route.ts b/apps/sim/app/api/jobs/[jobId]/route.ts index b096d801109..8dcfd8bafd2 100644 --- a/apps/sim/app/api/jobs/[jobId]/route.ts +++ b/apps/sim/app/api/jobs/[jobId]/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkHybridAuth } from '@/lib/auth/hybrid' import { getJobQueue } from '@/lib/core/async-jobs' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { createErrorResponse } from '@/app/api/workflows/utils' @@ -70,7 +71,7 @@ export async function GET( return NextResponse.json(response) } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message logger.error(`[${requestId}] Error fetching task status:`, error) if (errorMessage?.includes('not found')) { diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index f2bc6a2754f..422bee8e780 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -26,6 +26,7 @@ import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context' import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions' import { env } from '@/lib/core/config/env' import { RateLimiter } from '@/lib/core/rate-limiter' +import { toError } from '@/lib/core/utils/helpers' import { getBaseUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { @@ -231,7 +232,7 @@ class NextResponseCapture { try { handler() } catch (error) { - this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error))) + this.triggerErrorHandlers(toError(error)) } } } @@ -290,7 +291,7 @@ class NextResponseCapture { try { this._controller.enqueue(normalized) } catch (error) { - this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error))) + this.triggerErrorHandlers(toError(error)) } } else { this._pendingChunks.push(normalized) @@ -311,7 +312,7 @@ class NextResponseCapture { try { this._controller.close() } catch (error) { - this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error))) + this.triggerErrorHandlers(toError(error)) } } @@ -659,7 +660,7 @@ async function handleDirectToolCall( content: [ { type: 'text', - text: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`, + text: `Tool execution failed: ${toError(error).message}`, }, ], isError: true, @@ -740,7 +741,7 @@ async function handleBuildToolCall( logger.warn('Failed to generate workspace context for build tool call', { workflowId: resolved.workflowId, workspaceId: resolvedWorkspaceId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -789,7 +790,7 @@ async function handleBuildToolCall( content: [ { type: 'text', - text: `Build failed: ${error instanceof Error ? error.message : String(error)}`, + text: `Build failed: ${toError(error).message}`, }, ], isError: true, @@ -880,7 +881,7 @@ async function handleSubagentToolCall( content: [ { type: 'text', - text: `Subagent call failed: ${error instanceof Error ? error.message : String(error)}`, + text: `Subagent call failed: ${toError(error).message}`, }, ], isError: true, diff --git a/apps/sim/app/api/mcp/servers/[id]/refresh/route.ts b/apps/sim/app/api/mcp/servers/[id]/refresh/route.ts index b6b186ec4ac..7ddc8019d0e 100644 --- a/apps/sim/app/api/mcp/servers/[id]/refresh/route.ts +++ b/apps/sim/app/api/mcp/servers/[id]/refresh/route.ts @@ -3,6 +3,7 @@ import { mcpServers, workflow, workflowBlocks } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' +import { toError } from '@/lib/core/utils/helpers' import { withMcpAuth } from '@/lib/mcp/middleware' import { mcpService } from '@/lib/mcp/service' import type { McpServerStatusConfig, McpTool, McpToolSchema } from '@/lib/mcp/types' @@ -249,11 +250,7 @@ export const POST = withMcpAuth<{ id: string }>('read')( }) } catch (error) { logger.error(`[${requestId}] Error refreshing MCP server:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to refresh MCP server'), - 'Failed to refresh MCP server', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to refresh MCP server', 500) } } ) diff --git a/apps/sim/app/api/mcp/servers/[id]/route.ts b/apps/sim/app/api/mcp/servers/[id]/route.ts index 67c893fc754..93c4b3aca20 100644 --- a/apps/sim/app/api/mcp/servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/servers/[id]/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { toError } from '@/lib/core/utils/helpers' import { McpDnsResolutionError, McpDomainNotAllowedError, @@ -138,11 +139,7 @@ export const PATCH = withMcpAuth<{ id: string }>('write')( return createMcpSuccessResponse({ server: updatedServer }) } catch (error) { logger.error(`[${requestId}] Error updating MCP server:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to update MCP server'), - 'Failed to update MCP server', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to update MCP server', 500) } } ) diff --git a/apps/sim/app/api/mcp/servers/route.ts b/apps/sim/app/api/mcp/servers/route.ts index 5d5c1d8b6fe..b6ea4988035 100644 --- a/apps/sim/app/api/mcp/servers/route.ts +++ b/apps/sim/app/api/mcp/servers/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { McpDnsResolutionError, @@ -44,11 +45,7 @@ export const GET = withMcpAuth('read')( return createMcpSuccessResponse({ servers }) } catch (error) { logger.error(`[${requestId}] Error listing MCP servers:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to list MCP servers'), - 'Failed to list MCP servers', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to list MCP servers', 500) } } ) @@ -220,11 +217,7 @@ export const POST = withMcpAuth('write')( return createMcpSuccessResponse({ serverId }, 201) } catch (error) { logger.error(`[${requestId}] Error registering MCP server:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to register MCP server'), - 'Failed to register MCP server', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to register MCP server', 500) } } ) @@ -297,11 +290,7 @@ export const DELETE = withMcpAuth('admin')( return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` }) } catch (error) { logger.error(`[${requestId}] Error deleting MCP server:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to delete MCP server'), - 'Failed to delete MCP server', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to delete MCP server', 500) } } ) diff --git a/apps/sim/app/api/mcp/servers/test-connection/route.ts b/apps/sim/app/api/mcp/servers/test-connection/route.ts index 37d6696b9c0..2b4297d37ff 100644 --- a/apps/sim/app/api/mcp/servers/test-connection/route.ts +++ b/apps/sim/app/api/mcp/servers/test-connection/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' +import { toError } from '@/lib/core/utils/helpers' import { McpClient } from '@/lib/mcp/client' import { McpDnsResolutionError, @@ -220,11 +221,7 @@ export const POST = withMcpAuth('write')( return createMcpSuccessResponse(result, result.success ? 200 : 400) } catch (error) { logger.error(`[${requestId}] Error testing MCP server connection:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to test server connection'), - 'Failed to test server connection', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to test server connection', 500) } } ) diff --git a/apps/sim/app/api/mcp/tools/stored/route.ts b/apps/sim/app/api/mcp/tools/stored/route.ts index 5a5519c2777..8cb84a1362a 100644 --- a/apps/sim/app/api/mcp/tools/stored/route.ts +++ b/apps/sim/app/api/mcp/tools/stored/route.ts @@ -3,6 +3,7 @@ import { workflow, workflowBlocks } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' +import { toError } from '@/lib/core/utils/helpers' import { withMcpAuth } from '@/lib/mcp/middleware' import type { McpToolSchema, StoredMcpTool } from '@/lib/mcp/types' import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils' @@ -70,11 +71,7 @@ export const GET = withMcpAuth('read')( return createMcpSuccessResponse({ tools: storedTools }) } catch (error) { logger.error(`[${requestId}] Error fetching stored MCP tools:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to fetch stored MCP tools'), - 'Failed to fetch stored MCP tools', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to fetch stored MCP tools', 500) } } ) diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts index 6ed1bb2e0c6..57c1330a452 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { toError } from '@/lib/core/utils/helpers' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpPubSub } from '@/lib/mcp/pubsub' import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils' @@ -63,11 +64,7 @@ export const GET = withMcpAuth('read')( return createMcpSuccessResponse({ server, tools }) } catch (error) { logger.error(`[${requestId}] Error getting workflow MCP server:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to get workflow MCP server'), - 'Failed to get workflow MCP server', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to get workflow MCP server', 500) } } ) @@ -146,11 +143,7 @@ export const PATCH = withMcpAuth('write')( return createMcpSuccessResponse({ server: updatedServer }) } catch (error) { logger.error(`[${requestId}] Error updating workflow MCP server:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to update workflow MCP server'), - 'Failed to update workflow MCP server', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to update workflow MCP server', 500) } } ) @@ -201,11 +194,7 @@ export const DELETE = withMcpAuth('admin')( return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` }) } catch (error) { logger.error(`[${requestId}] Error deleting workflow MCP server:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to delete workflow MCP server'), - 'Failed to delete workflow MCP server', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to delete workflow MCP server', 500) } } ) diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts index 60791a36bcf..9d157b6a13c 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { toError } from '@/lib/core/utils/helpers' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpPubSub } from '@/lib/mcp/pubsub' import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils' @@ -63,11 +64,7 @@ export const GET = withMcpAuth('read')( return createMcpSuccessResponse({ tool }) } catch (error) { logger.error(`[${requestId}] Error getting tool:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to get tool'), - 'Failed to get tool', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to get tool', 500) } } ) @@ -164,11 +161,7 @@ export const PATCH = withMcpAuth('write')( return createMcpSuccessResponse({ tool: updatedTool }) } catch (error) { logger.error(`[${requestId}] Error updating tool:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to update tool'), - 'Failed to update tool', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to update tool', 500) } } ) @@ -232,11 +225,7 @@ export const DELETE = withMcpAuth('write')( return createMcpSuccessResponse({ message: `Tool ${toolId} deleted successfully` }) } catch (error) { logger.error(`[${requestId}] Error deleting tool:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to delete tool'), - 'Failed to delete tool', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to delete tool', 500) } } ) diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts index 396cfe92468..0e4e9608fda 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpPubSub } from '@/lib/mcp/pubsub' @@ -72,11 +73,7 @@ export const GET = withMcpAuth('read')( return createMcpSuccessResponse({ tools }) } catch (error) { logger.error(`[${requestId}] Error listing tools:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to list tools'), - 'Failed to list tools', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to list tools', 500) } } ) @@ -237,11 +234,7 @@ export const POST = withMcpAuth('write')( return createMcpSuccessResponse({ tool }, 201) } catch (error) { logger.error(`[${requestId}] Error adding tool:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to add tool'), - 'Failed to add tool', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to add tool', 500) } } ) diff --git a/apps/sim/app/api/mcp/workflow-servers/route.ts b/apps/sim/app/api/mcp/workflow-servers/route.ts index 807df769673..27a0032cd2f 100644 --- a/apps/sim/app/api/mcp/workflow-servers/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull, sql } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpPubSub } from '@/lib/mcp/pubsub' @@ -82,11 +83,7 @@ export const GET = withMcpAuth('read')( return createMcpSuccessResponse({ servers: serversWithToolNames }) } catch (error) { logger.error(`[${requestId}] Error listing workflow MCP servers:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to list workflow MCP servers'), - 'Failed to list workflow MCP servers', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to list workflow MCP servers', 500) } } ) @@ -221,11 +218,7 @@ export const POST = withMcpAuth('write')( return createMcpSuccessResponse({ server, addedTools }, 201) } catch (error) { logger.error(`[${requestId}] Error creating workflow MCP server:`, error) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Failed to create workflow MCP server'), - 'Failed to create workflow MCP server', - 500 - ) + return createMcpErrorResponse(toError(error), 'Failed to create workflow MCP server', 500) } } ) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/route.ts index cf94fdda83c..6be661ac09a 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/route.ts @@ -19,6 +19,7 @@ import { readEvents } from '@/lib/copilot/request/session/buffer' import { readFilePreviewSessions } from '@/lib/copilot/request/session/file-preview-session' import { type StreamBatchEvent, toStreamBatchEvent } from '@/lib/copilot/request/session/types' import { taskPubSub } from '@/lib/copilot/tasks' +import { toError } from '@/lib/core/utils/helpers' import { captureServerEvent } from '@/lib/posthog/server' const logger = createLogger('MothershipChatAPI') @@ -66,7 +67,7 @@ export async function GET( logger.warn('Failed to read preview sessions for mothership chat', { chatId, conversationId: chat.conversationId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return [] }), @@ -75,7 +76,7 @@ export async function GET( logger.warn('Failed to fetch latest run for mothership chat snapshot', { chatId, conversationId: chat.conversationId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null }) @@ -90,7 +91,7 @@ export async function GET( logger.warn('Failed to read stream snapshot for mothership chat', { chatId, conversationId: chat.conversationId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/app/api/mothership/execute/route.ts b/apps/sim/app/api/mothership/execute/route.ts index e0e3ac4c83c..3337838ce1e 100644 --- a/apps/sim/app/api/mothership/execute/route.ts +++ b/apps/sim/app/api/mothership/execute/route.ts @@ -6,6 +6,7 @@ import { buildIntegrationToolSchemas } from '@/lib/copilot/chat/payload' import { generateWorkspaceContext } from '@/lib/copilot/chat/workspace-context' import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' import { requestExplicitStreamAbort } from '@/lib/copilot/request/session/explicit-abort' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { assertActiveWorkspaceAccess, @@ -110,7 +111,7 @@ export async function POST(req: NextRequest) { chatId: effectiveChatId, }).catch((error) => { reqLogger.warn('Failed to send explicit abort for mothership execution', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) }) } diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index b1ad3317728..417be50c89e 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' import { @@ -112,7 +113,7 @@ export async function POST(request: NextRequest) { logger.error(`[${requestId}] Failed to resolve Vertex credential:`, { provider, model, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, hasVertexCredential: !!vertexCredential, }) return NextResponse.json( @@ -258,17 +259,14 @@ export async function POST(request: NextRequest) { } catch (error) { const executionTime = Date.now() - startTime logger.error(`[${requestId}] Provider request failed:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, errorName: error instanceof Error ? error.name : 'Unknown', errorStack: error instanceof Error ? error.stack : undefined, executionTime, timestamp: new Date().toISOString(), }) - return NextResponse.json( - { error: error instanceof Error ? error.message : String(error) }, - { status: 500 } - ) + return NextResponse.json({ error: toError(error).message }, { status: 500 }) } } diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts index 80832187d86..c97e5e5568b 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { AuthType } from '@/lib/auth/hybrid' import { getJobQueue } from '@/lib/core/async-jobs' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { SSE_HEADERS } from '@/lib/core/utils/sse' import { getBaseUrl } from '@/lib/core/utils/urls' @@ -235,7 +236,7 @@ export async function POST( }) } catch (dispatchError) { logger.error('Failed to dispatch async resume execution', { - error: dispatchError instanceof Error ? dispatchError.message : String(dispatchError), + error: toError(dispatchError).message, resumeExecutionId: enqueueResult.resumeExecutionId, }) return NextResponse.json( diff --git a/apps/sim/app/api/schedules/execute/route.ts b/apps/sim/app/api/schedules/execute/route.ts index 4748d1f4379..719f47df52e 100644 --- a/apps/sim/app/api/schedules/execute/route.ts +++ b/apps/sim/app/api/schedules/execute/route.ts @@ -4,6 +4,7 @@ import { and, eq, isNull, lt, lte, ne, not, or, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { generateId } from '@/lib/core/utils/uuid' import { @@ -136,7 +137,7 @@ export async function GET(request: NextRequest) { const output = await executeScheduleJob(payload) await jobQueue.completeJob(jobId, output) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message logger.error( `[${requestId}] Schedule execution failed for workflow ${schedule.workflowId}`, { @@ -191,7 +192,7 @@ export async function GET(request: NextRequest) { await executeJobInline(payload) } catch (error) { logger.error(`[${requestId}] Job execution failed for ${job.id}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) await releaseScheduleLock( job.id, diff --git a/apps/sim/app/api/table/[tableId]/import-csv/route.ts b/apps/sim/app/api/table/[tableId]/import-csv/route.ts index 7d6e32e5e38..3ad21468fec 100644 --- a/apps/sim/app/api/table/[tableId]/import-csv/route.ts +++ b/apps/sim/app/api/table/[tableId]/import-csv/route.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { generateId } from '@/lib/core/utils/uuid' import { @@ -163,7 +164,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { inserted += result.length } } catch (err) { - const message = err instanceof Error ? err.message : String(err) + const message = toError(err).message logger.warn(`[${requestId}] Append failed mid-import for table ${tableId}`, { inserted, total: coerced.length, @@ -238,7 +239,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }, }) } catch (err) { - const message = err instanceof Error ? err.message : String(err) + const message = toError(err).message const isClientError = message.includes('row limit') || message.includes('Schema validation') || @@ -251,7 +252,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { throw err } } catch (error) { - const message = error instanceof Error ? error.message : String(error) + const message = toError(error).message logger.error(`[${requestId}] CSV import into existing table failed:`, error) const isClientError = diff --git a/apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts b/apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts index 12326141c7d..63da0c91de0 100644 --- a/apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts @@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import type { RowData } from '@/lib/table' import { deleteRow, updateRow } from '@/lib/table' @@ -193,7 +194,7 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) { ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if (errorMessage === 'Row not found') { return NextResponse.json({ error: errorMessage }, { status: 404 }) @@ -260,7 +261,7 @@ export async function DELETE(request: NextRequest, { params }: RowRouteParams) { ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if (errorMessage === 'Row not found') { return NextResponse.json({ error: errorMessage }, { status: 404 }) diff --git a/apps/sim/app/api/table/[tableId]/rows/route.ts b/apps/sim/app/api/table/[tableId]/rows/route.ts index 305a55a8855..24def621927 100644 --- a/apps/sim/app/api/table/[tableId]/rows/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/route.ts @@ -5,6 +5,7 @@ import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import type { Filter, RowData, Sort, TableSchema } from '@/lib/table' import { @@ -181,7 +182,7 @@ async function handleBatchInsert( }, }) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if ( errorMessage.includes('row limit') || @@ -289,7 +290,7 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if ( errorMessage.includes('row limit') || @@ -516,7 +517,7 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if ( errorMessage.includes('Row size exceeds') || @@ -616,7 +617,7 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if (errorMessage.includes('Filter is required')) { return NextResponse.json({ error: errorMessage }, { status: 400 }) @@ -685,7 +686,7 @@ export async function PATCH(request: NextRequest, { params }: TableRowsRoutePara ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if ( errorMessage.includes('Row size exceeds') || diff --git a/apps/sim/app/api/table/[tableId]/rows/upsert/route.ts b/apps/sim/app/api/table/[tableId]/rows/upsert/route.ts index f78c90b2e0c..de510c7fdd9 100644 --- a/apps/sim/app/api/table/[tableId]/rows/upsert/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/upsert/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import type { RowData } from '@/lib/table' import { upsertRow } from '@/lib/table' @@ -87,7 +88,7 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams) ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message // Service layer throws descriptive errors for validation/capacity issues if ( diff --git a/apps/sim/app/api/table/import-csv/route.ts b/apps/sim/app/api/table/import-csv/route.ts index 7c8bc1389d1..0fe52910fbd 100644 --- a/apps/sim/app/api/table/import-csv/route.ts +++ b/apps/sim/app/api/table/import-csv/route.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { generateId } from '@/lib/core/utils/uuid' import { @@ -124,7 +125,7 @@ export async function POST(request: NextRequest) { throw insertError } } catch (error) { - const message = error instanceof Error ? error.message : String(error) + const message = toError(error).message logger.error(`[${requestId}] CSV import failed:`, error) const isClientError = diff --git a/apps/sim/app/api/templates/approved/sanitized/route.ts b/apps/sim/app/api/templates/approved/sanitized/route.ts index dd72ef80464..6d481e8a704 100644 --- a/apps/sim/app/api/templates/approved/sanitized/route.ts +++ b/apps/sim/app/api/templates/approved/sanitized/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalApiKey } from '@/lib/copilot/request/http' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer' @@ -97,7 +98,7 @@ export async function GET(request: NextRequest) { } } catch (error) { logger.error(`[${requestId}] Error sanitizing template ${template.id}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } @@ -112,7 +113,7 @@ export async function GET(request: NextRequest) { return NextResponse.json(response) } catch (error) { logger.error(`[${requestId}] Error fetching approved sanitized templates`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) return NextResponse.json( diff --git a/apps/sim/app/api/tools/a2a/send-message/route.ts b/apps/sim/app/api/tools/a2a/send-message/route.ts index c15d1921a31..0f4fa0445e2 100644 --- a/apps/sim/app/api/tools/a2a/send-message/route.ts +++ b/apps/sim/app/api/tools/a2a/send-message/route.ts @@ -5,6 +5,7 @@ import { z } from 'zod' import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { generateId } from '@/lib/core/utils/uuid' @@ -89,7 +90,7 @@ export async function POST(request: NextRequest) { parts.push(dataPart) } catch (parseError) { logger.warn(`[${requestId}] Failed to parse data as JSON, skipping DataPart`, { - error: parseError instanceof Error ? parseError.message : String(parseError), + error: toError(parseError).message, }) } } diff --git a/apps/sim/app/api/tools/agiloft/attach/route.ts b/apps/sim/app/api/tools/agiloft/attach/route.ts index db55283d82a..27c704f6979 100644 --- a/apps/sim/app/api/tools/agiloft/attach/route.ts +++ b/apps/sim/app/api/tools/agiloft/attach/route.ts @@ -2,12 +2,17 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' -import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' +import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { FileInputSchema, type RawFileInput } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' -import { agiloftLogin, agiloftLogout, buildAttachFileUrl } from '@/tools/agiloft/utils' +import { + agiloftLogin, + agiloftLogout, + buildAttachFileUrl, + validateInstanceUrl, +} from '@/tools/agiloft/utils' export const dynamic = 'force-dynamic' @@ -60,18 +65,20 @@ export async function POST(request: NextRequest) { const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) const resolvedFileName = data.fileName || userFile.name || 'attachment' - const urlValidation = await validateUrlWithDNS(data.instanceUrl, 'instanceUrl') - if (!urlValidation.isValid) { + let resolvedIP: string + try { + resolvedIP = await validateInstanceUrl(data.instanceUrl) + } catch (error) { logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, { instanceUrl: data.instanceUrl, }) return NextResponse.json( - { success: false, error: urlValidation.error || 'Invalid instance URL' }, + { success: false, error: error instanceof Error ? error.message : 'Invalid instance URL' }, { status: 400 } ) } - const token = await agiloftLogin(data) + const token = await agiloftLogin(data, resolvedIP) const base = data.instanceUrl.replace(/\/$/, '') try { @@ -79,7 +86,7 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Uploading file to Agiloft: ${resolvedFileName}`) - const agiloftResponse = await fetch(url, { + const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'PUT', headers: { 'Content-Type': userFile.type || 'application/octet-stream', @@ -123,7 +130,7 @@ export async function POST(request: NextRequest) { }, }) } finally { - await agiloftLogout(data.instanceUrl, data.knowledgeBase, token) + await agiloftLogout(data.instanceUrl, data.knowledgeBase, token, resolvedIP) } } catch (error) { if (error instanceof z.ZodError) { diff --git a/apps/sim/app/api/tools/asana/create-task/route.ts b/apps/sim/app/api/tools/asana/create-task/route.ts index 7d82ff3e47f..ddcdacdb5cc 100644 --- a/apps/sim/app/api/tools/asana/create-task/route.ts +++ b/apps/sim/app/api/tools/asana/create-task/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' export const dynamic = 'force-dynamic' @@ -115,7 +116,7 @@ export async function POST(request: NextRequest) { }) } catch (error: any) { logger.error('Error creating Asana task:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/asana/update-task/route.ts b/apps/sim/app/api/tools/asana/update-task/route.ts index 8d990cf58b1..8831ed1d856 100644 --- a/apps/sim/app/api/tools/asana/update-task/route.ts +++ b/apps/sim/app/api/tools/asana/update-task/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' export const dynamic = 'force-dynamic' @@ -114,7 +115,7 @@ export async function PUT(request: NextRequest) { }) } catch (error: any) { logger.error('Error updating Asana task:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/cloudwatch/utils.ts b/apps/sim/app/api/tools/cloudwatch/utils.ts index 47db3b541da..4c074955298 100644 --- a/apps/sim/app/api/tools/cloudwatch/utils.ts +++ b/apps/sim/app/api/tools/cloudwatch/utils.ts @@ -6,6 +6,7 @@ import { type ResultField, } from '@aws-sdk/client-cloudwatch-logs' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { sleep } from '@/lib/core/utils/helpers' interface AwsCredentials { region: string @@ -79,7 +80,7 @@ export async function pollQueryResults( throw new Error(`CloudWatch Log Insights query ${status.toLowerCase()}`) } - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)) + await sleep(pollIntervalMs) } // Timeout -- fetch one last time for partial results diff --git a/apps/sim/app/api/tools/image/route.ts b/apps/sim/app/api/tools/image/route.ts index 475a9de5c63..9fba35ad721 100644 --- a/apps/sim/app/api/tools/image/route.ts +++ b/apps/sim/app/api/tools/image/route.ts @@ -5,6 +5,7 @@ import { secureFetchWithPinnedIP, validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' const logger = createLogger('ImageProxyAPI') @@ -83,7 +84,7 @@ export async function GET(request: NextRequest) { }, }) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message logger.error(`[${requestId}] Image proxy error:`, { error: errorMessage }) return new NextResponse(`Failed to proxy image: ${errorMessage}`, { diff --git a/apps/sim/app/api/tools/jira/update/route.ts b/apps/sim/app/api/tools/jira/update/route.ts index 2c0f5dcb4ab..74ccc0f42a9 100644 --- a/apps/sim/app/api/tools/jira/update/route.ts +++ b/apps/sim/app/api/tools/jira/update/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage, toAdf } from '@/tools/jira/utils' export const dynamic = 'force-dynamic' @@ -182,7 +183,7 @@ export async function PUT(request: NextRequest) { }) } catch (error: any) { logger.error('Error updating Jira issue:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jira/write/route.ts b/apps/sim/app/api/tools/jira/write/route.ts index d63689b267f..b54d24993b2 100644 --- a/apps/sim/app/api/tools/jira/write/route.ts +++ b/apps/sim/app/api/tools/jira/write/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage, toAdf } from '@/tools/jira/utils' export const dynamic = 'force-dynamic' @@ -225,7 +226,7 @@ export async function POST(request: NextRequest) { }) } catch (error: any) { logger.error('Error creating Jira issue:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/approvals/route.ts b/apps/sim/app/api/tools/jsm/approvals/route.ts index 099c9483c5a..387f1b6dc83 100644 --- a/apps/sim/app/api/tools/jsm/approvals/route.ts +++ b/apps/sim/app/api/tools/jsm/approvals/route.ts @@ -7,6 +7,7 @@ import { validateJiraCloudId, validateJiraIssueKey, } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -199,7 +200,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Invalid action' }, { status: 400 }) } catch (error) { logger.error('Error in approvals operation:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/comment/route.ts b/apps/sim/app/api/tools/jsm/comment/route.ts index 4d119ff27b2..7a1758c5125 100644 --- a/apps/sim/app/api/tools/jsm/comment/route.ts +++ b/apps/sim/app/api/tools/jsm/comment/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -112,7 +113,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error adding comment:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/comments/route.ts b/apps/sim/app/api/tools/jsm/comments/route.ts index ee849088717..43bbd8eefcf 100644 --- a/apps/sim/app/api/tools/jsm/comments/route.ts +++ b/apps/sim/app/api/tools/jsm/comments/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -105,7 +106,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching comments:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/customers/route.ts b/apps/sim/app/api/tools/jsm/customers/route.ts index a44041d7bb3..67cff52bd04 100644 --- a/apps/sim/app/api/tools/jsm/customers/route.ts +++ b/apps/sim/app/api/tools/jsm/customers/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -157,7 +158,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error with customers operation:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/answers/route.ts b/apps/sim/app/api/tools/jsm/forms/answers/route.ts index d7b079df831..5d529f0c076 100644 --- a/apps/sim/app/api/tools/jsm/forms/answers/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/answers/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -96,7 +97,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error getting form answers:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/attach/route.ts b/apps/sim/app/api/tools/jsm/forms/attach/route.ts index dee8b1549cc..2e33cc7afe2 100644 --- a/apps/sim/app/api/tools/jsm/forms/attach/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/attach/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -104,7 +105,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error attaching form:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/copy/route.ts b/apps/sim/app/api/tools/jsm/forms/copy/route.ts index 7872735c05b..0f65f9de63a 100644 --- a/apps/sim/app/api/tools/jsm/forms/copy/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/copy/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -111,7 +112,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error copying forms:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/delete/route.ts b/apps/sim/app/api/tools/jsm/forms/delete/route.ts index d5942c4d764..28ff73a017d 100644 --- a/apps/sim/app/api/tools/jsm/forms/delete/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/delete/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -96,7 +97,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error deleting form:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/externalise/route.ts b/apps/sim/app/api/tools/jsm/forms/externalise/route.ts index 71edbcc0f5e..5922bfc68b3 100644 --- a/apps/sim/app/api/tools/jsm/forms/externalise/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/externalise/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -97,7 +98,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error externalising form:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/get/route.ts b/apps/sim/app/api/tools/jsm/forms/get/route.ts index d73cf9c4c59..35ca42a6687 100644 --- a/apps/sim/app/api/tools/jsm/forms/get/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/get/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -98,7 +99,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error getting form:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/internalise/route.ts b/apps/sim/app/api/tools/jsm/forms/internalise/route.ts index 9524207376e..8d8aae7201b 100644 --- a/apps/sim/app/api/tools/jsm/forms/internalise/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/internalise/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -97,7 +98,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error internalising form:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/issue/route.ts b/apps/sim/app/api/tools/jsm/forms/issue/route.ts index 1405ca8a267..0d504359db0 100644 --- a/apps/sim/app/api/tools/jsm/forms/issue/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/issue/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -96,7 +97,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching issue forms:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/reopen/route.ts b/apps/sim/app/api/tools/jsm/forms/reopen/route.ts index afadc124847..333d9c347b4 100644 --- a/apps/sim/app/api/tools/jsm/forms/reopen/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/reopen/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -97,7 +98,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error reopening form:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/save/route.ts b/apps/sim/app/api/tools/jsm/forms/save/route.ts index 97f8c91c58e..7869ac80246 100644 --- a/apps/sim/app/api/tools/jsm/forms/save/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/save/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -103,7 +104,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error saving form answers:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/structure/route.ts b/apps/sim/app/api/tools/jsm/forms/structure/route.ts index f224d67970f..51fdb78e4f6 100644 --- a/apps/sim/app/api/tools/jsm/forms/structure/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/structure/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -98,7 +99,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching form structure:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/submit/route.ts b/apps/sim/app/api/tools/jsm/forms/submit/route.ts index 38d50189d2e..d2c92b6e600 100644 --- a/apps/sim/app/api/tools/jsm/forms/submit/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/submit/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -97,7 +98,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error submitting form:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/forms/templates/route.ts b/apps/sim/app/api/tools/jsm/forms/templates/route.ts index 65695b7d639..a79703b703e 100644 --- a/apps/sim/app/api/tools/jsm/forms/templates/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/templates/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -96,7 +97,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching form templates:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/organization/route.ts b/apps/sim/app/api/tools/jsm/organization/route.ts index 15b4f7302e1..a2bfd2ca40d 100644 --- a/apps/sim/app/api/tools/jsm/organization/route.ts +++ b/apps/sim/app/api/tools/jsm/organization/route.ts @@ -6,6 +6,7 @@ import { validateEnum, validateJiraCloudId, } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -169,7 +170,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Invalid action' }, { status: 400 }) } catch (error) { logger.error('Error in organization operation:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/organizations/route.ts b/apps/sim/app/api/tools/jsm/organizations/route.ts index 52fb6699673..b963de9f55e 100644 --- a/apps/sim/app/api/tools/jsm/organizations/route.ts +++ b/apps/sim/app/api/tools/jsm/organizations/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -91,7 +92,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching organizations:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/participants/route.ts b/apps/sim/app/api/tools/jsm/participants/route.ts index ea9c31d55cc..6188181400e 100644 --- a/apps/sim/app/api/tools/jsm/participants/route.ts +++ b/apps/sim/app/api/tools/jsm/participants/route.ts @@ -6,6 +6,7 @@ import { validateJiraCloudId, validateJiraIssueKey, } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -174,7 +175,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Invalid action' }, { status: 400 }) } catch (error) { logger.error('Error in participants operation:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/queues/route.ts b/apps/sim/app/api/tools/jsm/queues/route.ts index fa3d7ad14df..b70f8924cd1 100644 --- a/apps/sim/app/api/tools/jsm/queues/route.ts +++ b/apps/sim/app/api/tools/jsm/queues/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -100,7 +101,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching queues:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/request/route.ts b/apps/sim/app/api/tools/jsm/request/route.ts index 858c68beaae..924d9fa6cb3 100644 --- a/apps/sim/app/api/tools/jsm/request/route.ts +++ b/apps/sim/app/api/tools/jsm/request/route.ts @@ -6,6 +6,7 @@ import { validateJiraCloudId, validateJiraIssueKey, } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -250,7 +251,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error with request operation:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/requests/route.ts b/apps/sim/app/api/tools/jsm/requests/route.ts index f53f9c6d79c..511e482490b 100644 --- a/apps/sim/app/api/tools/jsm/requests/route.ts +++ b/apps/sim/app/api/tools/jsm/requests/route.ts @@ -6,6 +6,7 @@ import { validateEnum, validateJiraCloudId, } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -140,7 +141,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching requests:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/requesttypefields/route.ts b/apps/sim/app/api/tools/jsm/requesttypefields/route.ts index 99ee19a0838..da14d3e3dc1 100644 --- a/apps/sim/app/api/tools/jsm/requesttypefields/route.ts +++ b/apps/sim/app/api/tools/jsm/requesttypefields/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -108,7 +109,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching request type fields:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/requesttypes/route.ts b/apps/sim/app/api/tools/jsm/requesttypes/route.ts index 964d9aab9d1..2f2513eabfe 100644 --- a/apps/sim/app/api/tools/jsm/requesttypes/route.ts +++ b/apps/sim/app/api/tools/jsm/requesttypes/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -104,7 +105,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching request types:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/servicedesks/route.ts b/apps/sim/app/api/tools/jsm/servicedesks/route.ts index 7470a17aeff..74d60f9623f 100644 --- a/apps/sim/app/api/tools/jsm/servicedesks/route.ts +++ b/apps/sim/app/api/tools/jsm/servicedesks/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -82,7 +83,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching service desks:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/sla/route.ts b/apps/sim/app/api/tools/jsm/sla/route.ts index 7ea1144f59c..7484a9d8527 100644 --- a/apps/sim/app/api/tools/jsm/sla/route.ts +++ b/apps/sim/app/api/tools/jsm/sla/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -92,7 +93,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching SLA info:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/transition/route.ts b/apps/sim/app/api/tools/jsm/transition/route.ts index 2e7a8743da5..39ac559c8e7 100644 --- a/apps/sim/app/api/tools/jsm/transition/route.ts +++ b/apps/sim/app/api/tools/jsm/transition/route.ts @@ -6,6 +6,7 @@ import { validateJiraCloudId, validateJiraIssueKey, } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -116,7 +117,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error transitioning request:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/jsm/transitions/route.ts b/apps/sim/app/api/tools/jsm/transitions/route.ts index d8027a88201..ed017b0a5c4 100644 --- a/apps/sim/app/api/tools/jsm/transitions/route.ts +++ b/apps/sim/app/api/tools/jsm/transitions/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' @@ -92,7 +93,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { logger.error('Error fetching transitions:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) diff --git a/apps/sim/app/api/tools/microsoft-teams/channels/route.ts b/apps/sim/app/api/tools/microsoft-teams/channels/route.ts index 0dc1fa8a0a4..5d2ca796499 100644 --- a/apps/sim/app/api/tools/microsoft-teams/channels/route.ts +++ b/apps/sim/app/api/tools/microsoft-teams/channels/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' export const dynamic = 'force-dynamic' @@ -99,7 +100,7 @@ export async function POST(request: Request) { } catch (innerError) { logger.error('Error during API requests:', innerError) - const errorMessage = innerError instanceof Error ? innerError.message : String(innerError) + const errorMessage = toError(innerError).message if ( errorMessage.includes('auth') || errorMessage.includes('token') || diff --git a/apps/sim/app/api/tools/microsoft-teams/chats/route.ts b/apps/sim/app/api/tools/microsoft-teams/chats/route.ts index a0113647a61..26ad9f8b33c 100644 --- a/apps/sim/app/api/tools/microsoft-teams/chats/route.ts +++ b/apps/sim/app/api/tools/microsoft-teams/chats/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' export const dynamic = 'force-dynamic' @@ -110,15 +111,13 @@ const getChatDisplayName = async ( } } catch (error) { logger.warn( - `Failed to get better name from messages for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}` + `Failed to get better name from messages for chat ${chatId}: ${toError(error).message}` ) } return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...` } catch (error) { - logger.warn( - `Failed to get display name for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}` - ) + logger.warn(`Failed to get display name for chat ${chatId}: ${toError(error).message}`) return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...` } } @@ -200,7 +199,7 @@ export async function POST(request: Request) { } catch (innerError) { logger.error('Error during API requests:', innerError) - const errorMessage = innerError instanceof Error ? innerError.message : String(innerError) + const errorMessage = toError(innerError).message if ( errorMessage.includes('auth') || errorMessage.includes('token') || diff --git a/apps/sim/app/api/tools/microsoft-teams/teams/route.ts b/apps/sim/app/api/tools/microsoft-teams/teams/route.ts index a903815abe2..7045af36358 100644 --- a/apps/sim/app/api/tools/microsoft-teams/teams/route.ts +++ b/apps/sim/app/api/tools/microsoft-teams/teams/route.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -89,7 +90,7 @@ export async function POST(request: Request) { logger.error('Error during API requests:', innerError) // Check if it's an authentication error - const errorMessage = innerError instanceof Error ? innerError.message : String(innerError) + const errorMessage = toError(innerError).message if ( errorMessage.includes('auth') || errorMessage.includes('token') || diff --git a/apps/sim/app/api/tools/onepassword/utils.ts b/apps/sim/app/api/tools/onepassword/utils.ts index b4efe69d516..07f4a43d11e 100644 --- a/apps/sim/app/api/tools/onepassword/utils.ts +++ b/apps/sim/app/api/tools/onepassword/utils.ts @@ -12,6 +12,7 @@ import type { import { createLogger } from '@sim/logger' import * as ipaddr from 'ipaddr.js' import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' /** Connect-format field type strings returned by normalization. */ type ConnectFieldType = @@ -283,7 +284,7 @@ async function validateConnectServerUrl(serverUrl: string): Promise { if (error instanceof Error && error.message.startsWith('1Password')) throw error connectLogger.warn('DNS lookup failed for 1Password Connect server URL', { hostname: clean, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) throw new Error('1Password server URL hostname could not be resolved') } diff --git a/apps/sim/app/api/tools/outlook/folders/route.ts b/apps/sim/app/api/tools/outlook/folders/route.ts index 8bf9e906d17..26fa0e9da80 100644 --- a/apps/sim/app/api/tools/outlook/folders/route.ts +++ b/apps/sim/app/api/tools/outlook/folders/route.ts @@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' @@ -135,7 +136,7 @@ export async function GET(request: Request) { } catch (innerError) { logger.error('Error during API requests:', innerError) - const errorMessage = innerError instanceof Error ? innerError.message : String(innerError) + const errorMessage = toError(innerError).message if ( errorMessage.includes('auth') || errorMessage.includes('token') || diff --git a/apps/sim/app/api/tools/sftp/utils.ts b/apps/sim/app/api/tools/sftp/utils.ts index 72468a21b10..094c784ac28 100644 --- a/apps/sim/app/api/tools/sftp/utils.ts +++ b/apps/sim/app/api/tools/sftp/utils.ts @@ -1,5 +1,6 @@ import { type Attributes, Client, type ConnectConfig, type SFTPWrapper } from 'ssh2' import { validateDatabaseHost } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' const S_IFMT = 0o170000 const S_IFDIR = 0o040000 @@ -151,7 +152,7 @@ export async function createSftpConnection(config: SftpConnectionConfig): Promis try { client.connect(connectConfig) } catch (err) { - reject(formatSftpError(err instanceof Error ? err : new Error(String(err)), { host, port })) + reject(formatSftpError(toError(err), { host, port })) } }) } diff --git a/apps/sim/app/api/tools/smtp/send/route.ts b/apps/sim/app/api/tools/smtp/send/route.ts index dc5c4fb28c0..796b7114b3c 100644 --- a/apps/sim/app/api/tools/smtp/send/route.ts +++ b/apps/sim/app/api/tools/smtp/send/route.ts @@ -4,6 +4,7 @@ import nodemailer from 'nodemailer' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateDatabaseHost } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -223,7 +224,7 @@ export async function POST(request: NextRequest) { } logger.error(`[${requestId}] Error sending email via SMTP:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, code: isNodeError(error) ? error.code : undefined, responseCode: hasResponseCode(error) ? error.responseCode : undefined, }) diff --git a/apps/sim/app/api/tools/ssh/utils.ts b/apps/sim/app/api/tools/ssh/utils.ts index 9561924c718..a5506419cfa 100644 --- a/apps/sim/app/api/tools/ssh/utils.ts +++ b/apps/sim/app/api/tools/ssh/utils.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { type Attributes, Client, type ConnectConfig } from 'ssh2' import { validateDatabaseHost } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('SSHUtils') @@ -168,7 +169,7 @@ export async function createSSHConnection(config: SSHConnectionConfig): Promise< try { client.connect(connectConfig) } catch (err) { - reject(formatSSHError(err instanceof Error ? err : new Error(String(err)), { host, port })) + reject(formatSSHError(toError(err), { host, port })) } }) } diff --git a/apps/sim/app/api/tools/stt/route.ts b/apps/sim/app/api/tools/stt/route.ts index e45bb273f06..fbbd8abdb93 100644 --- a/apps/sim/app/api/tools/stt/route.ts +++ b/apps/sim/app/api/tools/stt/route.ts @@ -7,6 +7,7 @@ import { secureFetchWithPinnedIP, validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' +import { sleep } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { getMimeTypeFromExtension, isInternalFileUrl } from '@/lib/uploads/utils/file-utils' import { @@ -663,7 +664,7 @@ async function transcribeWithAssemblyAI( throw new Error(`AssemblyAI transcription failed: ${transcript.error}`) } - await new Promise((resolve) => setTimeout(resolve, 5000)) + await sleep(5000) attempts++ } diff --git a/apps/sim/app/api/tools/supabase/storage-upload/route.ts b/apps/sim/app/api/tools/supabase/storage-upload/route.ts index c0677bb35aa..a8795be41ca 100644 --- a/apps/sim/app/api/tools/supabase/storage-upload/route.ts +++ b/apps/sim/app/api/tools/supabase/storage-upload/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateSupabaseProjectId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { FileInputSchema } from '@/lib/uploads/utils/file-schemas' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' @@ -12,7 +13,10 @@ export const dynamic = 'force-dynamic' const logger = createLogger('SupabaseStorageUploadAPI') const SupabaseStorageUploadSchema = z.object({ - projectId: z.string().min(1, 'Project ID is required'), + projectId: z + .string() + .min(1, 'Project ID is required') + .regex(/^[a-z0-9]+$/, 'Project ID must contain only lowercase alphanumeric characters'), apiKey: z.string().min(1, 'API key is required'), bucket: z.string().min(1, 'Bucket name is required'), fileName: z.string().min(1, 'File name is required'), @@ -162,7 +166,12 @@ export async function POST(request: NextRequest) { fullPath = `${folderPath}${validatedData.fileName}` } - const supabaseUrl = `https://${validatedData.projectId}.supabase.co/storage/v1/object/${validatedData.bucket}/${fullPath}` + const projectValidation = validateSupabaseProjectId(validatedData.projectId) + if (!projectValidation.isValid) { + return NextResponse.json({ success: false, error: projectValidation.error }, { status: 400 }) + } + + const supabaseUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/${validatedData.bucket}/${fullPath}` const headers: Record = { apikey: validatedData.apiKey, @@ -218,7 +227,7 @@ export async function POST(request: NextRequest) { path: fullPath, }) - const publicUrl = `https://${validatedData.projectId}.supabase.co/storage/v1/object/public/${validatedData.bucket}/${fullPath}` + const publicUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/public/${validatedData.bucket}/${fullPath}` return NextResponse.json({ success: true, diff --git a/apps/sim/app/api/tools/textract/parse/route.ts b/apps/sim/app/api/tools/textract/parse/route.ts index c1986a4de90..3191e428860 100644 --- a/apps/sim/app/api/tools/textract/parse/route.ts +++ b/apps/sim/app/api/tools/textract/parse/route.ts @@ -9,6 +9,7 @@ import { secureFetchWithPinnedIP, validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' +import { sleep } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' import { isInternalFileUrl, processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' @@ -169,10 +170,6 @@ function parseS3Uri(s3Uri: string): { bucket: string; key: string } { return { bucket, key } } -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - async function callTextractAsync( host: string, amzTarget: string, diff --git a/apps/sim/app/api/tools/video/route.ts b/apps/sim/app/api/tools/video/route.ts index 3a5a5b817f7..c8cad166824 100644 --- a/apps/sim/app/api/tools/video/route.ts +++ b/apps/sim/app/api/tools/video/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' +import { sleep } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import type { UserFile } from '@/executor/types' @@ -974,7 +975,3 @@ function getVideoDimensions( return { width, height } } - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) -} diff --git a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts b/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts index fad7ddb9a81..96af579f452 100644 --- a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts +++ b/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts @@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' import { hasPaidSubscription } from '@/lib/billing' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('SubscriptionTransferAPI') @@ -114,7 +115,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ }) } catch (error) { logger.error('Error transferring subscription', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return NextResponse.json({ error: 'Failed to transfer subscription' }, { status: 500 }) } diff --git a/apps/sim/app/api/users/me/usage-logs/route.ts b/apps/sim/app/api/users/me/usage-logs/route.ts index e95b6fc03ae..9de44c4d01e 100644 --- a/apps/sim/app/api/users/me/usage-logs/route.ts +++ b/apps/sim/app/api/users/me/usage-logs/route.ts @@ -4,6 +4,7 @@ import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log' import { dollarsToCredits } from '@/lib/billing/credits/conversion' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('UsageLogsAPI') @@ -109,7 +110,7 @@ export async function GET(req: NextRequest) { }) } catch (error) { logger.error('Failed to get usage logs', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return NextResponse.json( diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index d7af2d8cdc7..3fed69a78bb 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { COPILOT_REQUEST_MODES } from '@/lib/copilot/constants' import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { getWorkflowById, resolveWorkflowIdForUser } from '@/lib/workflows/utils' import { authenticateV1Request } from '@/app/api/v1/auth' @@ -136,7 +137,7 @@ export async function POST(req: NextRequest) { ? `Headless copilot request failed [messageId:${messageId}]` : 'Headless copilot request failed', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, } ) return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 }) diff --git a/apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts b/apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts index bc7901a80d0..c8712e44de7 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import type { RowData } from '@/lib/table' import { updateRow } from '@/lib/table' @@ -196,7 +197,7 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) { ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if (errorMessage === 'Row not found') { return NextResponse.json({ error: errorMessage }, { status: 404 }) diff --git a/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts b/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts index 8021625b1b8..ee5a91bf436 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import type { Filter, RowData, Sort, TableSchema } from '@/lib/table' import { @@ -162,7 +163,7 @@ async function handleBatchInsert( }, }) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if ( errorMessage.includes('row limit') || @@ -392,7 +393,7 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if ( errorMessage.includes('row limit') || @@ -489,7 +490,7 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if ( errorMessage.includes('Row size exceeds') || @@ -591,7 +592,7 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if (errorMessage.includes('Filter is required')) { return NextResponse.json({ error: errorMessage }, { status: 400 }) diff --git a/apps/sim/app/api/v1/tables/[tableId]/rows/upsert/route.ts b/apps/sim/app/api/v1/tables/[tableId]/rows/upsert/route.ts index 93f1351a8f2..13045436233 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/rows/upsert/route.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/rows/upsert/route.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import type { RowData } from '@/lib/table' import { upsertRow } from '@/lib/table' @@ -99,7 +100,7 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams) ) } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if ( errorMessage.includes('unique column') || diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 2f9a72cf1eb..307e79cc710 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -9,6 +9,7 @@ import { getTimeoutErrorMessage, isTimeoutError, } from '@/lib/core/execution-limits' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { SSE_HEADERS } from '@/lib/core/utils/sse' import { getBaseUrl } from '@/lib/core/utils/urls' @@ -218,7 +219,7 @@ async function handleAsyncExecution(params: AsyncExecutionParams): Promise setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) if (closed) return const newEvents = await readExecutionEvents(executionId, lastEventId) @@ -135,7 +136,7 @@ export async function GET( } catch (error) { logger.error('Error in reconnection stream', { executionId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) if (!closed) { try { diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index b9b7e91a79a..0260a1129d9 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { env } from '@/lib/core/config/env' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { getSocketServerUrl } from '@/lib/core/utils/urls' import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence' @@ -150,7 +151,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } catch (error) { logger.error('Failed to fetch workflow state', { workflowId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts index c1d0c930339..a2cf4ad4848 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts @@ -12,6 +12,7 @@ import { import { getSession } from '@/lib/auth' import { decryptSecret } from '@/lib/core/security/encryption' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' import { getBaseUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { sendEmail } from '@/lib/messaging/email/mailer' @@ -159,7 +160,7 @@ async function testWebhook(subscription: typeof workspaceNotificationSubscriptio } } catch (error: unknown) { logger.warn('Webhook test failed', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return { success: false, error: 'Failed to deliver webhook' } } @@ -273,7 +274,7 @@ async function testSlack( } } catch (error: unknown) { logger.warn('Slack test notification failed', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return { success: false, error: 'Failed to send Slack notification' } } diff --git a/apps/sim/app/chat/components/message/components/file-download.tsx b/apps/sim/app/chat/components/message/components/file-download.tsx index 8bd501bc69c..fc148fc9da4 100644 --- a/apps/sim/app/chat/components/message/components/file-download.tsx +++ b/apps/sim/app/chat/components/message/components/file-download.tsx @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { ArrowDown, Download, Loader2, Music } from 'lucide-react' import { Button } from '@/components/emcn' import { DefaultFileIcon, getDocumentIcon } from '@/components/icons/document-icons' +import { sleep } from '@/lib/core/utils/helpers' import type { ChatFile } from '@/app/chat/components/message/message' const logger = createLogger('ChatFileDownload') @@ -157,7 +158,7 @@ export function ChatFileDownloadAll({ files }: ChatFileDownloadAllProps) { logger.info(`Downloaded file ${i + 1}/${files.length}: ${file.name}`) if (i < files.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 150)) + await sleep(150) } } catch (error) { logger.error(`Failed to download file ${file.name}:`, error) diff --git a/apps/sim/app/ingest/[[...path]]/route.ts b/apps/sim/app/ingest/[[...path]]/route.ts index 39c537bc93a..9084c5c0f41 100644 --- a/apps/sim/app/ingest/[[...path]]/route.ts +++ b/apps/sim/app/ingest/[[...path]]/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('PostHogProxy') @@ -59,7 +60,7 @@ async function handler(request: NextRequest) { logger.error('PostHog proxy error', { url, method: request.method, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return new NextResponse(null, { status: 502 }) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 8575e9a1b44..bcdad8292e7 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -86,6 +86,7 @@ import { reportManualRunToolStop, } from '@/lib/copilot/tools/client/run-tool-execution' import { isWorkflowToolName } from '@/lib/copilot/tools/workflow-tools' +import { sleep, toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { getNextWorkflowColor } from '@/lib/workflows/colors' import { getQueryClient } from '@/app/_shell/providers/get-query-client' @@ -2512,7 +2513,7 @@ export function useChat( } catch (error) { logger.warn('Failed to load chat history while recovering stream', { chatId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } @@ -2746,7 +2747,7 @@ export function useChat( if (isStaleReconnect()) return true setTransportReconnecting() - await new Promise((resolve) => setTimeout(resolve, delayMs)) + await sleep(delayMs) if (streamGenRef.current !== gen) { if (!sendingRef.current) { setTransportIdle() @@ -2816,7 +2817,7 @@ export function useChat( logger.warn('Reconnect attempt failed', { streamId, attempt: attempt + 1, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) } } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts index 265f3f0c7f4..a38c476c46e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts @@ -1,6 +1,7 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' +import { sleep } from '@/lib/core/utils/helpers' import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' import { knowledgeKeys } from '@/hooks/queries/kb/knowledge' @@ -111,11 +112,6 @@ const calculateUploadTimeoutMs = (fileSize: number) => { return Math.min(dynamicBudget, UPLOAD_CONFIG.MAX_TIMEOUT_MS) } -/** - * Delays execution for the specified duration - */ -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) - /** * Gets high resolution timestamp for performance measurements */ diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/usage-limit/usage-limit.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/usage-limit/usage-limit.tsx index f0be6e21397..ba7ea7ca72b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/usage-limit/usage-limit.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/usage-limit/usage-limit.tsx @@ -7,6 +7,7 @@ import { Badge, Button } from '@/components/emcn' import { ON_DEMAND_UNLIMITED } from '@/lib/billing/constants' import { formatCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' +import { toError } from '@/lib/core/utils/helpers' import { useUpdateOrganizationUsageLimit } from '@/hooks/queries/organization' import { useUpdateUsageLimit } from '@/hooks/queries/subscription' @@ -140,7 +141,7 @@ export const UsageLimit = forwardRef( } catch (err) { logger.error('Failed to update usage limit', { error: err }) - const message = err instanceof Error ? err.message : String(err) + const message = toError(err).message if (message.includes('below current usage')) { setErrorType('belowUsage') } else { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index e881ed300dd..ec140ce4759 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -34,6 +34,7 @@ import { import { Lock, Unlock, Upload } from '@/components/emcn/icons' import { VariableIcon } from '@/components/icons' import { useSession } from '@/lib/auth/auth-client' +import { toError } from '@/lib/core/utils/helpers' import { captureEvent } from '@/lib/posthog/client' import { generateWorkflowJson } from '@/lib/workflows/operations/import-export' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components' @@ -320,7 +321,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel }) .catch((err) => { logger.error('Failed to fetch/apply edit_workflow state', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, workflowId, }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 32959c8a321..4bbb03a420c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' +import { sleep, toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { processStreamingBlockLogs } from '@/lib/tokenization' @@ -296,7 +297,7 @@ export function useWorkflowExecution() { async (error: any, operation: string) => { logger.error(`Debug ${operation} Error:`, error) - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message const errorResult = { success: false, output: {}, @@ -1901,7 +1902,7 @@ export function useWorkflowExecution() { if (attempt > 0) { const delay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS) - await new Promise((resolve) => setTimeout(resolve, delay)) + await sleep(delay) if (cleanupRan || reconnectionComplete) return } diff --git a/apps/sim/background/resume-execution.ts b/apps/sim/background/resume-execution.ts index 5831b1c1580..dfdd03a3c24 100644 --- a/apps/sim/background/resume-execution.ts +++ b/apps/sim/background/resume-execution.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { task } from '@trigger.dev/sdk' +import { toError } from '@/lib/core/utils/helpers' import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' const logger = createLogger('TriggerResumeExecution') @@ -61,7 +62,7 @@ export async function executeResumeJob(payload: ResumeExecutionPayload) { logger.error('Background resume execution failed', { resumeExecutionId, workflowId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) throw error } diff --git a/apps/sim/background/schedule-execution.ts b/apps/sim/background/schedule-execution.ts index 634adcaf753..54c1f10a7f0 100644 --- a/apps/sim/background/schedule-execution.ts +++ b/apps/sim/background/schedule-execution.ts @@ -5,6 +5,7 @@ import { Cron } from 'croner' import { and, eq, isNull } from 'drizzle-orm' import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types' import { createTimeoutAbortController, getTimeoutErrorMessage } from '@/lib/core/execution-limits' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { preprocessExecution } from '@/lib/execution/preprocessing' import { LoggingSession } from '@/lib/logs/execution/logging-session' @@ -270,7 +271,7 @@ async function runWorkflowExecution({ await loggingSession.safeCompleteWithError({ error: { - message: error instanceof Error ? error.message : String(error), + message: toError(error).message, stackTrace: error instanceof Error ? error.stack : undefined, }, traceSpans, @@ -597,7 +598,7 @@ export async function executeScheduleJob(payload: ScheduleExecutionPayload) { `Error updating schedule ${payload.scheduleId} after failure` ) } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if (errorMessage.includes('Service overloaded')) { logger.warn(`[${requestId}] Service overloaded, retrying schedule in 5 minutes`) @@ -833,7 +834,7 @@ async function createJobLogEntry(params: { }) } catch (error) { logger.error('Failed to create job log entry', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -1009,7 +1010,7 @@ export async function executeJobInline(payload: JobExecutionPayload) { `Error updating job ${payload.scheduleId} after success` ) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message logger.error(`[${requestId}] Job execution failed`, { scheduleId: payload.scheduleId, error: errorMessage, diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index 843de822019..ff33c23d1ad 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -6,6 +6,7 @@ import { eq } from 'drizzle-orm' import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types' import { createTimeoutAbortController, getTimeoutErrorMessage } from '@/lib/core/execution-limits' import { IdempotencyService, webhookIdempotency } from '@/lib/core/idempotency' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { preprocessExecution } from '@/lib/execution/preprocessing' import { LoggingSession } from '@/lib/logs/execution/logging-session' @@ -180,7 +181,7 @@ export async function resolveWebhookExecutionProviderConfig< try { return await resolveWebhookRecordProviderConfig(webhookRecord, userId, workspaceId) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error( `Failed to resolve webhook provider config for ${provider} webhook ${webhookRecord.id}: ${errorMessage}` ) @@ -502,7 +503,7 @@ async function executeWebhookJobInternal( provider: payload.provider, } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message const errorStack = error instanceof Error ? error.stack : undefined logger.error(`[${requestId}] Webhook execution failed`, { diff --git a/apps/sim/background/workflow-execution.ts b/apps/sim/background/workflow-execution.ts index 794b12d0195..428fe3e7c8c 100644 --- a/apps/sim/background/workflow-execution.ts +++ b/apps/sim/background/workflow-execution.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { task } from '@trigger.dev/sdk' import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types' import { createTimeoutAbortController, getTimeoutErrorMessage } from '@/lib/core/execution-limits' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { preprocessExecution } from '@/lib/execution/preprocessing' import { LoggingSession } from '@/lib/logs/execution/logging-session' @@ -172,7 +173,7 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) { } } catch (error: unknown) { logger.error(`[${requestId}] Workflow execution failed: ${workflowId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, executionId, }) @@ -185,7 +186,7 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) { await loggingSession.safeCompleteWithError({ error: { - message: error instanceof Error ? error.message : String(error), + message: toError(error).message, stackTrace: error instanceof Error ? error.stack : undefined, }, traceSpans, diff --git a/apps/sim/background/workspace-notification-delivery.ts b/apps/sim/background/workspace-notification-delivery.ts index 7bdde512299..17fcf73012b 100644 --- a/apps/sim/background/workspace-notification-delivery.ts +++ b/apps/sim/background/workspace-notification-delivery.ts @@ -20,6 +20,7 @@ import { RateLimiter } from '@/lib/core/rate-limiter' import { decryptSecret } from '@/lib/core/security/encryption' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { formatDuration } from '@/lib/core/utils/formatting' +import { toError } from '@/lib/core/utils/helpers' import { getBaseUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' @@ -225,7 +226,7 @@ async function deliverWebhook( } } catch (error: unknown) { logger.warn('Webhook delivery failed', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, webhookUrl: webhookConfig.url, }) return { diff --git a/apps/sim/blocks/blocks/langsmith.ts b/apps/sim/blocks/blocks/langsmith.ts index 96773f8fc7b..8e5ae00f2ee 100644 --- a/apps/sim/blocks/blocks/langsmith.ts +++ b/apps/sim/blocks/blocks/langsmith.ts @@ -1,4 +1,5 @@ import { LangsmithIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' import type { LangsmithResponse } from '@/tools/langsmith/types' @@ -221,9 +222,7 @@ Common patch fields: outputs, end_time, status, error`, try { return JSON.parse(value) } catch (error) { - throw new Error( - `Invalid JSON for ${label}: ${error instanceof Error ? error.message : String(error)}` - ) + throw new Error(`Invalid JSON for ${label}: ${toError(error).message}`) } } return value diff --git a/apps/sim/blocks/blocks/mistral_parse.ts b/apps/sim/blocks/blocks/mistral_parse.ts index 2ee52dbcf82..a6b40b2861f 100644 --- a/apps/sim/blocks/blocks/mistral_parse.ts +++ b/apps/sim/blocks/blocks/mistral_parse.ts @@ -1,4 +1,5 @@ import { MistralIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { AuthMode, type BlockConfig, IntegrationType, type SubBlockType } from '@/blocks/types' import { createVersionedToolSelector, normalizeFileInput } from '@/blocks/utils' import type { MistralParserOutput } from '@/tools/mistral/types' @@ -118,7 +119,7 @@ export const MistralParseBlock: BlockConfig = { pagesArray = undefined } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error(`Page number format error: ${errorMessage}`) } } @@ -248,7 +249,7 @@ export const MistralParseV2Block: BlockConfig = { pagesArray = undefined } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error(`Page number format error: ${errorMessage}`) } } @@ -371,7 +372,7 @@ export const MistralParseV3Block: BlockConfig = { pagesArray = undefined } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error(`Page number format error: ${errorMessage}`) } } diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 34f5198aeeb..7b015b03d9f 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -1,4 +1,5 @@ import { NotionIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import type { BlockConfig } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import { createVersionedToolSelector } from '@/blocks/utils' @@ -345,9 +346,7 @@ export const NotionBlock: BlockConfig = { try { parsedProperties = JSON.parse(properties) } catch (error) { - throw new Error( - `Invalid JSON for properties: ${error instanceof Error ? error.message : String(error)}` - ) + throw new Error(`Invalid JSON for properties: ${toError(error).message}`) } } else { parsedProperties = properties @@ -360,9 +359,7 @@ export const NotionBlock: BlockConfig = { try { parsedFilter = JSON.parse(filter) } catch (error) { - throw new Error( - `Invalid JSON for filter: ${error instanceof Error ? error.message : String(error)}` - ) + throw new Error(`Invalid JSON for filter: ${toError(error).message}`) } } @@ -372,9 +369,7 @@ export const NotionBlock: BlockConfig = { try { parsedSorts = JSON.parse(sorts) } catch (error) { - throw new Error( - `Invalid JSON for sorts: ${error instanceof Error ? error.message : String(error)}` - ) + throw new Error(`Invalid JSON for sorts: ${toError(error).message}`) } } diff --git a/apps/sim/blocks/blocks/reducto.ts b/apps/sim/blocks/blocks/reducto.ts index cd666fa739c..83314b4d59f 100644 --- a/apps/sim/blocks/blocks/reducto.ts +++ b/apps/sim/blocks/blocks/reducto.ts @@ -1,4 +1,5 @@ import { ReductoIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { AuthMode, type BlockConfig, IntegrationType, type SubBlockType } from '@/blocks/types' import { createVersionedToolSelector, normalizeFileInput } from '@/blocks/utils' import type { ReductoParserOutput } from '@/tools/reducto/types' @@ -98,7 +99,7 @@ export const ReductoBlock: BlockConfig = { pagesArray = undefined } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error(`Page number format error: ${errorMessage}`) } } @@ -203,7 +204,7 @@ export const ReductoV2Block: BlockConfig = { pagesArray = undefined } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error(`Page number format error: ${errorMessage}`) } } diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 8813654588d..70ebf85dfd8 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { MicrosoftSharepointIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' @@ -439,7 +440,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, parsedItemFields = JSON.parse(listItemFields) } catch (error) { logger.error('Failed to parse listItemFields JSON', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/blocks/blocks/table.ts b/apps/sim/blocks/blocks/table.ts index a903cf3b3e4..0d027eacc98 100644 --- a/apps/sim/blocks/blocks/table.ts +++ b/apps/sim/blocks/blocks/table.ts @@ -1,4 +1,5 @@ import { TableIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { TABLE_LIMITS } from '@/lib/table/constants' import { filterRulesToFilter, sortRulesToSort } from '@/lib/table/query-builder/converters' import type { BlockConfig } from '@/blocks/types' @@ -20,7 +21,7 @@ function parseJSON(value: string | unknown, fieldName: string): unknown { try { return JSON.parse(value) } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error) + const errorMsg = toError(error).message // Check if the error might be due to unquoted string values // This happens when users write {"field": } instead of {"field": ""} diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 64ce0eb7a7f..148d2ad8eef 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -1,4 +1,5 @@ import { isAzureConfigured, isHosted, isOllamaConfigured } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' import { @@ -385,9 +386,7 @@ export function parseOptionalJsonInput(value: unknown, label: strin try { return JSON.parse(trimmed) as T } catch (error) { - throw new Error( - `Invalid JSON for ${label}: ${error instanceof Error ? error.message : String(error)}` - ) + throw new Error(`Invalid JSON for ${label}: ${toError(error).message}`) } } diff --git a/apps/sim/connectors/airtable/airtable.ts b/apps/sim/connectors/airtable/airtable.ts index 84ac42380ac..6ef383c68ba 100644 --- a/apps/sim/connectors/airtable/airtable.ts +++ b/apps/sim/connectors/airtable/airtable.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { AirtableIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { computeContentHash, parseTagDate } from '@/connectors/utils' @@ -404,7 +405,7 @@ async function fetchFieldNames( } } catch (error) { logger.warn('Error fetching Airtable schema', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } diff --git a/apps/sim/connectors/asana/asana.ts b/apps/sim/connectors/asana/asana.ts index 25009ec5c4a..1cb4c22d788 100644 --- a/apps/sim/connectors/asana/asana.ts +++ b/apps/sim/connectors/asana/asana.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { AsanaIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { joinTagArray, parseTagDate } from '@/connectors/utils' @@ -333,7 +334,7 @@ export const asanaConnector: ConnectorConfig = { } catch (error) { logger.error('Failed to get Asana task', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/confluence/confluence.ts b/apps/sim/connectors/confluence/confluence.ts index 2ba7b71e038..583647ffe6b 100644 --- a/apps/sim/connectors/confluence/confluence.ts +++ b/apps/sim/connectors/confluence/confluence.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { ConfluenceIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, joinTagArray, parseTagDate } from '@/connectors/utils' @@ -63,7 +64,7 @@ async function fetchLabelsForPages( return { pageId, labels } } catch (error) { logger.warn(`Error fetching labels for page ${pageId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return { pageId, labels: [] as string[] } } diff --git a/apps/sim/connectors/discord/discord.ts b/apps/sim/connectors/discord/discord.ts index d67a45deefd..2da531584fd 100644 --- a/apps/sim/connectors/discord/discord.ts +++ b/apps/sim/connectors/discord/discord.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { DiscordIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { computeContentHash, parseTagDate } from '@/connectors/utils' @@ -268,7 +269,7 @@ export const discordConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Discord channel document', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/dropbox/dropbox.ts b/apps/sim/connectors/dropbox/dropbox.ts index 87cbced05b4..3bcba681819 100644 --- a/apps/sim/connectors/dropbox/dropbox.ts +++ b/apps/sim/connectors/dropbox/dropbox.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { DropboxIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, parseTagDate } from '@/connectors/utils' @@ -246,7 +247,7 @@ export const dropboxConnector: ConnectorConfig = { return { ...stub, content, contentDeferred: false } } catch (error) { logger.warn(`Failed to fetch document ${externalId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/evernote/evernote.ts b/apps/sim/connectors/evernote/evernote.ts index c84adf6b0c2..a6e23d15326 100644 --- a/apps/sim/connectors/evernote/evernote.ts +++ b/apps/sim/connectors/evernote/evernote.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { EvernoteIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { ThriftReader, @@ -509,7 +510,7 @@ export const evernoteConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Evernote note', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/fireflies/fireflies.ts b/apps/sim/connectors/fireflies/fireflies.ts index c09cccb426d..65341a324db 100644 --- a/apps/sim/connectors/fireflies/fireflies.ts +++ b/apps/sim/connectors/fireflies/fireflies.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { FirefliesIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { parseTagDate } from '@/connectors/utils' @@ -305,7 +306,7 @@ export const firefliesConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Fireflies transcript', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/github/github.ts b/apps/sim/connectors/github/github.ts index 5ffabc4229d..b11dba3cbb3 100644 --- a/apps/sim/connectors/github/github.ts +++ b/apps/sim/connectors/github/github.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { GithubIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { parseTagDate } from '@/connectors/utils' @@ -280,7 +281,7 @@ export const githubConnector: ConnectorConfig = { } } catch (error) { logger.warn(`Failed to fetch GitHub document ${externalId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/gmail/gmail.ts b/apps/sim/connectors/gmail/gmail.ts index 68a8f6c888f..a671df4f9d1 100644 --- a/apps/sim/connectors/gmail/gmail.ts +++ b/apps/sim/connectors/gmail/gmail.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { GmailIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, joinTagArray, parseTagDate } from '@/connectors/utils' @@ -493,7 +494,7 @@ export const gmailConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Gmail thread', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/google-docs/google-docs.ts b/apps/sim/connectors/google-docs/google-docs.ts index 4f4269df2b6..244f4364159 100644 --- a/apps/sim/connectors/google-docs/google-docs.ts +++ b/apps/sim/connectors/google-docs/google-docs.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { GoogleDocsIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { joinTagArray, parseTagDate } from '@/connectors/utils' @@ -283,7 +284,7 @@ export const googleDocsConnector: ConnectorConfig = { return { ...fileToStub(file), content, contentDeferred: false } } catch (error) { logger.warn(`Failed to extract content from document: ${file.name} (${file.id})`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/google-drive/google-drive.ts b/apps/sim/connectors/google-drive/google-drive.ts index cb6190c4dbe..dd3c2f26d28 100644 --- a/apps/sim/connectors/google-drive/google-drive.ts +++ b/apps/sim/connectors/google-drive/google-drive.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { GoogleDriveIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, joinTagArray, parseTagDate } from '@/connectors/utils' @@ -319,7 +320,7 @@ export const googleDriveConnector: ConnectorConfig = { return { ...stub, content, contentDeferred: false } } catch (error) { logger.warn(`Failed to fetch content for file: ${file.name} (${file.id})`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/google-sheets/google-sheets.ts b/apps/sim/connectors/google-sheets/google-sheets.ts index 2fb1c1f2865..2b81bc1485e 100644 --- a/apps/sim/connectors/google-sheets/google-sheets.ts +++ b/apps/sim/connectors/google-sheets/google-sheets.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { GoogleSheetsIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { parseTagDate } from '@/connectors/utils' @@ -129,7 +130,7 @@ async function fetchSpreadsheetModifiedTime( return data.modifiedTime } catch (error) { logger.warn('Error fetching modifiedTime from Drive API', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return undefined } @@ -189,7 +190,7 @@ async function sheetToDocument( } } catch (error) { logger.warn(`Failed to extract content from sheet: ${sheet.title}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } @@ -310,7 +311,7 @@ export const googleSheetsConnector: ConnectorConfig = { fetchSpreadsheetModifiedTime(accessToken, spreadsheetId), ]) } catch (error) { - const message = error instanceof Error ? error.message : String(error) + const message = toError(error).message if (message.includes('404')) { logger.info('Spreadsheet not found (possibly deleted)', { spreadsheetId }) return null diff --git a/apps/sim/connectors/intercom/intercom.ts b/apps/sim/connectors/intercom/intercom.ts index e7e011d83a3..3af1d4a43dd 100644 --- a/apps/sim/connectors/intercom/intercom.ts +++ b/apps/sim/connectors/intercom/intercom.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { IntercomIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, parseTagDate } from '@/connectors/utils' @@ -430,7 +431,7 @@ export const intercomConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Intercom document', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/linear/linear.ts b/apps/sim/connectors/linear/linear.ts index efe391c7c00..e5f2727e7d6 100644 --- a/apps/sim/connectors/linear/linear.ts +++ b/apps/sim/connectors/linear/linear.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { LinearIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import type { RetryOptions } from '@/lib/knowledge/documents/utils' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' @@ -361,7 +362,7 @@ export const linearConnector: ConnectorConfig = { } catch (error) { logger.error('Failed to get Linear issue', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/microsoft-teams/microsoft-teams.ts b/apps/sim/connectors/microsoft-teams/microsoft-teams.ts index d47f49bc43b..65310ac438b 100644 --- a/apps/sim/connectors/microsoft-teams/microsoft-teams.ts +++ b/apps/sim/connectors/microsoft-teams/microsoft-teams.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { MicrosoftTeamsIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { computeContentHash, htmlToPlainText, parseTagDate } from '@/connectors/utils' @@ -359,7 +360,7 @@ export const microsoftTeamsConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Microsoft Teams channel document', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/notion/notion.ts b/apps/sim/connectors/notion/notion.ts index 4b56cc41f12..c049c0fbcda 100644 --- a/apps/sim/connectors/notion/notion.ts +++ b/apps/sim/connectors/notion/notion.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NotionIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { joinTagArray, parseTagDate } from '@/connectors/utils' @@ -292,7 +293,7 @@ export const notionConnector: ConnectorConfig = { return { ...stub, content, contentDeferred: false } } catch (error) { logger.warn(`Failed to fetch content for Notion page: ${externalId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } @@ -594,7 +595,7 @@ async function listFromParentPage( return pageToStub(page) } catch (error) { logger.warn(`Failed to process child page ${pageId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/obsidian/obsidian.ts b/apps/sim/connectors/obsidian/obsidian.ts index b6f7a3013f3..387ec66d1bf 100644 --- a/apps/sim/connectors/obsidian/obsidian.ts +++ b/apps/sim/connectors/obsidian/obsidian.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { ObsidianIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { joinTagArray, parseTagDate } from '@/connectors/utils' @@ -100,7 +101,7 @@ async function listVaultFiles( } catch (error) { logger.warn('Failed to list subdirectory', { dir, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -259,7 +260,7 @@ export const obsidianConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Obsidian note', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/onedrive/onedrive.ts b/apps/sim/connectors/onedrive/onedrive.ts index 74037741e11..ac68c3fce51 100644 --- a/apps/sim/connectors/onedrive/onedrive.ts +++ b/apps/sim/connectors/onedrive/onedrive.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { MicrosoftOneDriveIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, parseTagDate } from '@/connectors/utils' @@ -298,7 +299,7 @@ export const onedriveConnector: ConnectorConfig = { return { ...stub, content, contentDeferred: false } } catch (error) { logger.warn(`Failed to fetch content for file: ${item.name} (${item.id})`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/outlook/outlook.ts b/apps/sim/connectors/outlook/outlook.ts index d36635d6d9b..e219e10dd0a 100644 --- a/apps/sim/connectors/outlook/outlook.ts +++ b/apps/sim/connectors/outlook/outlook.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { OutlookIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, parseTagDate } from '@/connectors/utils' @@ -550,7 +551,7 @@ export const outlookConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Outlook conversation', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/reddit/reddit.ts b/apps/sim/connectors/reddit/reddit.ts index ce636ad1be7..894c1f8b272 100644 --- a/apps/sim/connectors/reddit/reddit.ts +++ b/apps/sim/connectors/reddit/reddit.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { RedditIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { parseTagDate } from '@/connectors/utils' @@ -107,7 +108,7 @@ async function fetchPostComments( } catch (error) { logger.warn('Failed to fetch comments for post', { postId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return [] } @@ -412,7 +413,7 @@ export const redditConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Reddit post document', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/servicenow/servicenow.ts b/apps/sim/connectors/servicenow/servicenow.ts index ebd33e79056..35436a5e0e4 100644 --- a/apps/sim/connectors/servicenow/servicenow.ts +++ b/apps/sim/connectors/servicenow/servicenow.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { ServiceNowIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, parseTagDate } from '@/connectors/utils' @@ -536,7 +537,7 @@ export const servicenowConnector: ConnectorConfig = { logger.warn('Failed to get ServiceNow document', { externalId, table: tableName, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/sharepoint/sharepoint.ts b/apps/sim/connectors/sharepoint/sharepoint.ts index 2c79820b3d1..62b12a0a8b9 100644 --- a/apps/sim/connectors/sharepoint/sharepoint.ts +++ b/apps/sim/connectors/sharepoint/sharepoint.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { MicrosoftSharepointIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, parseTagDate } from '@/connectors/utils' @@ -473,7 +474,7 @@ export const sharepointConnector: ConnectorConfig = { return { ...stub, content, contentDeferred: false } } catch (error) { logger.warn(`Failed to fetch content for file: ${item.name} (${item.id})`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/slack/slack.ts b/apps/sim/connectors/slack/slack.ts index d0a3e0233f3..5434acfe570 100644 --- a/apps/sim/connectors/slack/slack.ts +++ b/apps/sim/connectors/slack/slack.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { SlackIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { computeContentHash, parseTagDate } from '@/connectors/utils' @@ -108,7 +109,7 @@ async function resolveUserName( } catch (error) { logger.warn('Failed to resolve Slack user name', { userId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return userId } @@ -413,7 +414,7 @@ export const slackConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Slack channel document', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/webflow/webflow.ts b/apps/sim/connectors/webflow/webflow.ts index b0ee6932fac..718e72f9827 100644 --- a/apps/sim/connectors/webflow/webflow.ts +++ b/apps/sim/connectors/webflow/webflow.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { WebflowIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, parseTagDate } from '@/connectors/utils' @@ -480,7 +481,7 @@ async function fetchCollectionNameDirect( } catch (error) { logger.warn('Error fetching collection name', { collectionId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return collectionId } diff --git a/apps/sim/connectors/wordpress/wordpress.ts b/apps/sim/connectors/wordpress/wordpress.ts index eb079f104a9..4f59da0697d 100644 --- a/apps/sim/connectors/wordpress/wordpress.ts +++ b/apps/sim/connectors/wordpress/wordpress.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { WordpressIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, joinTagArray, parseTagDate } from '@/connectors/utils' @@ -230,7 +231,7 @@ export const wordpressConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get WordPress document', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/connectors/zendesk/zendesk.ts b/apps/sim/connectors/zendesk/zendesk.ts index 6bdf57e6108..c2e6c653b16 100644 --- a/apps/sim/connectors/zendesk/zendesk.ts +++ b/apps/sim/connectors/zendesk/zendesk.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { ZendeskIcon } from '@/components/icons' +import { toError } from '@/lib/core/utils/helpers' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { htmlToPlainText, joinTagArray, parseTagDate } from '@/connectors/utils' @@ -459,7 +460,7 @@ export const zendeskConnector: ConnectorConfig = { } catch (error) { logger.warn('Failed to get Zendesk document', { externalId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/executor/dag/construction/edges.ts b/apps/sim/executor/dag/construction/edges.ts index f6fd1c83617..0dd86c11f3a 100644 --- a/apps/sim/executor/dag/construction/edges.ts +++ b/apps/sim/executor/dag/construction/edges.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { EDGE, isConditionBlockType, @@ -113,7 +114,7 @@ export class EdgeConstructor { } catch (error) { logger.warn('Failed to parse condition config', { blockId: block.id, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null @@ -136,7 +137,7 @@ export class EdgeConstructor { } catch (error) { logger.warn('Failed to parse router v2 config', { blockId: block.id, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 5044eab5639..55b5df5ac4c 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -1,5 +1,6 @@ import { createLogger, type Logger } from '@sim/logger' import { redactApiKeys } from '@/lib/core/security/redaction' +import { toError } from '@/lib/core/utils/helpers' import { getBaseUrl } from '@/lib/core/utils/urls' import { containsUserFileWithMetadata, @@ -475,7 +476,7 @@ export class BlockExecutor { this.execLogger.warn('Block start callback failed', { blockId, blockType, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -521,7 +522,7 @@ export class BlockExecutor { this.execLogger.warn('Block completion callback failed', { blockId, blockType, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/executor/execution/engine.ts b/apps/sim/executor/execution/engine.ts index 756ab0a03b3..bf110b7bdb6 100644 --- a/apps/sim/executor/execution/engine.ts +++ b/apps/sim/executor/execution/engine.ts @@ -1,4 +1,5 @@ import { createLogger, type Logger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { isExecutionCancelled, isRedisCancellationEnabled } from '@/lib/execution/cancellation' import { BlockType } from '@/executor/constants' import type { DAG } from '@/executor/dag/builder' @@ -226,7 +227,7 @@ export class ExecutionEngine { .catch((error) => { if (!this.errorFlag) { this.errorFlag = true - this.executionError = error instanceof Error ? error : new Error(String(error)) + this.executionError = toError(error) } }) .finally(() => { @@ -509,7 +510,7 @@ export class ExecutionEngine { return parsedSnapshot.state } catch (error) { this.execLogger.warn('Failed to serialize execution state', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return undefined } diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 6df3c7e9c6f..09572c43e86 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull } from 'drizzle-orm' +import { toError } from '@/lib/core/utils/helpers' import { createMcpToolId } from '@/lib/mcp/utils' import { getCustomToolById } from '@/lib/workflows/custom-tools/operations' import { getAllBlocks } from '@/blocks' @@ -468,7 +469,7 @@ export class AgentBlockHandler implements BlockHandler { return data.data.tools } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error) + const errorMsg = toError(error).message if (this.isRetryableError(errorMsg) && attempt < maxAttempts - 1) { logger.warn( `[AgentHandler] Retryable error discovering tools from ${serverId} (attempt ${attempt + 1}):`, diff --git a/apps/sim/executor/handlers/generic/generic-handler.ts b/apps/sim/executor/handlers/generic/generic-handler.ts index 6c9e1bb53ac..64a915c16f8 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { getBlock } from '@/blocks/index' import { isMcpTool } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' @@ -48,7 +49,7 @@ export class GenericBlockHandler implements BlockHandler { finalInputs[key] = JSON.parse(value.trim()) } catch (error) { logger.warn(`Failed to parse ${inputType} field "${key}":`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/executor/handlers/mothership/mothership-handler.ts b/apps/sim/executor/handlers/mothership/mothership-handler.ts index 8f98ce4404e..9b788f847b0 100644 --- a/apps/sim/executor/handlers/mothership/mothership-handler.ts +++ b/apps/sim/executor/handlers/mothership/mothership-handler.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { isExecutionCancelled, isRedisCancellationEnabled } from '@/lib/execution/cancellation' import type { BlockOutput } from '@/blocks/types' @@ -91,7 +92,7 @@ export class MothershipBlockHandler implements BlockHandler { logger.warn('Failed to poll workflow cancellation for Mothership block', { blockId: block.id, executionId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) }) .finally(() => { diff --git a/apps/sim/executor/orchestrators/loop.ts b/apps/sim/executor/orchestrators/loop.ts index eb94c668764..d01daaa67a5 100644 --- a/apps/sim/executor/orchestrators/loop.ts +++ b/apps/sim/executor/orchestrators/loop.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { isExecutionCancelled, isRedisCancellationEnabled } from '@/lib/execution/cancellation' import { executeInIsolatedVM } from '@/lib/execution/isolated-vm' @@ -134,7 +135,7 @@ export class LoopOrchestrator { try { items = resolveArrayInput(ctx, loopConfig.forEachItems, this.resolver) } catch (error) { - const errorMessage = `ForEach loop resolution failed: ${error instanceof Error ? error.message : String(error)}` + const errorMessage = `ForEach loop resolution failed: ${toError(error).message}` logger.error(errorMessage, { loopId, forEachItems: loopConfig.forEachItems }) await this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, { forEachItems: loopConfig.forEachItems, diff --git a/apps/sim/executor/orchestrators/parallel.ts b/apps/sim/executor/orchestrators/parallel.ts index 2af4308fe62..1f98953bc5e 100644 --- a/apps/sim/executor/orchestrators/parallel.ts +++ b/apps/sim/executor/orchestrators/parallel.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { DEFAULTS } from '@/executor/constants' import type { DAG } from '@/executor/dag/builder' import type { ParallelScope } from '@/executor/execution/state' @@ -68,7 +69,7 @@ export class ParallelOrchestrator { items = resolved.items isEmpty = resolved.isEmpty ?? false } catch (error) { - const baseErrorMessage = error instanceof Error ? error.message : String(error) + const baseErrorMessage = toError(error).message const errorMessage = baseErrorMessage.startsWith('Parallel collection distribution is empty') ? baseErrorMessage : `Parallel Items did not resolve: ${baseErrorMessage}` diff --git a/apps/sim/executor/utils/file-tool-processor.ts b/apps/sim/executor/utils/file-tool-processor.ts index 1f0b10374b3..1b1e168415b 100644 --- a/apps/sim/executor/utils/file-tool-processor.ts +++ b/apps/sim/executor/utils/file-tool-processor.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { isUserFile } from '@/lib/core/utils/user-file' import { uploadExecutionFile, uploadFileFromRawData } from '@/lib/uploads/contexts/execution' import { downloadFileFromUrl } from '@/lib/uploads/utils/file-utils.server' @@ -47,7 +48,7 @@ export class FileToolProcessor { ) } catch (error) { logger.error(`Error processing file output '${outputKey}':`, error) - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error(`Failed to process file output '${outputKey}': ${errorMessage}`) } } diff --git a/apps/sim/executor/utils/subflow-utils.ts b/apps/sim/executor/utils/subflow-utils.ts index 99570329bf5..b7227bd4ad8 100644 --- a/apps/sim/executor/utils/subflow-utils.ts +++ b/apps/sim/executor/utils/subflow-utils.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { DEFAULTS, LOOP, PARALLEL, REFERENCE } from '@/executor/constants' import type { ContextExtensions } from '@/executor/execution/types' import { type BlockLog, type ExecutionContext, getNextExecutionOrder } from '@/executor/types' @@ -224,9 +225,7 @@ export function resolveArrayInput( if (error instanceof Error && error.message.startsWith('Reference "')) { throw error } - throw new Error( - `Failed to resolve reference "${items}": ${error instanceof Error ? error.message : String(error)}` - ) + throw new Error(`Failed to resolve reference "${items}": ${toError(error).message}`) } } @@ -259,9 +258,7 @@ export function resolveArrayInput( if (error instanceof Error && error.message.startsWith('Resolved items')) { throw error } - throw new Error( - `Failed to resolve items: ${error instanceof Error ? error.message : String(error)}` - ) + throw new Error(`Failed to resolve items: ${toError(error).message}`) } } @@ -308,7 +305,7 @@ export async function addSubflowErrorLog( logger.warn('Subflow error start callback failed', { blockId, blockType, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -327,7 +324,7 @@ export async function addSubflowErrorLog( logger.warn('Subflow error completion callback failed', { blockId, blockType, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -370,7 +367,7 @@ export async function emitEmptySubflowEvents( logger.warn('Empty subflow start callback failed', { blockId, blockType, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -394,7 +391,7 @@ export async function emitEmptySubflowEvents( logger.warn('Empty subflow completion callback failed', { blockId, blockType, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -451,7 +448,7 @@ export async function emitSubflowSuccessEvents( logger.warn('Subflow success completion callback failed', { blockId, blockType, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index 88b23d72340..88198fc360f 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { BlockType } from '@/executor/constants' import type { ExecutionState, LoopScope } from '@/executor/execution/state' import type { ExecutionContext } from '@/executor/types' @@ -191,7 +192,7 @@ export class VariableResolver { return this.blockResolver.formatValueForBlock(resolved, blockType, language) } catch (error) { - replacementError = error instanceof Error ? error : new Error(String(error)) + replacementError = toError(error) return match } }) @@ -250,7 +251,7 @@ export class VariableResolver { } return String(resolved) } catch (error) { - replacementError = error instanceof Error ? error : new Error(String(error)) + replacementError = toError(error) return match } }) diff --git a/apps/sim/lib/a2a/utils.ts b/apps/sim/lib/a2a/utils.ts index 9dd1b879050..f9b122eddbe 100644 --- a/apps/sim/lib/a2a/utils.ts +++ b/apps/sim/lib/a2a/utils.ts @@ -17,6 +17,7 @@ import { } from '@a2a-js/sdk/client' import { createLogger } from '@sim/logger' import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { isInternalFileUrl } from '@/lib/uploads/utils/file-utils' import { A2A_TERMINAL_STATES } from './constants' @@ -74,7 +75,7 @@ export async function createA2AClient(agentUrl: string, apiKey?: string): Promis } catch (standardError) { logger.debug('Standard agent card path failed, trying root URL', { agentUrl, - error: standardError instanceof Error ? standardError.message : String(standardError), + error: toError(standardError).message, }) } diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 53632d8330f..461c02f8460 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -73,6 +73,7 @@ import { isSignupEmailValidationEnabled, } from '@/lib/core/config/feature-flags' import { PlatformEvents } from '@/lib/core/telemetry' +import { toError } from '@/lib/core/utils/helpers' import { getBaseUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { processCredentialDraft } from '@/lib/credentials/draft-processor' @@ -796,7 +797,7 @@ export const auth = betterAuth({ } catch (err) { logger.warn('CIMD resolution failed', { clientId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) } } @@ -2920,7 +2921,7 @@ export const auth = betterAuth({ referenceId: subscription.referenceId, dbPlan: subscription.plan, planFromStripe, - error: orgError instanceof Error ? orgError.message : String(orgError), + error: toError(orgError).message, stack: orgError instanceof Error ? orgError.stack : undefined, } ) @@ -3010,7 +3011,7 @@ export const auth = betterAuth({ dbPlan: subscription.plan, planFromStripe, isUpgradeToTeam, - error: orgError instanceof Error ? orgError.message : String(orgError), + error: toError(orgError).message, stack: orgError instanceof Error ? orgError.stack : undefined, } ) diff --git a/apps/sim/lib/auth/cimd.ts b/apps/sim/lib/auth/cimd.ts index 8a4990fd850..3ed57e50cc7 100644 --- a/apps/sim/lib/auth/cimd.ts +++ b/apps/sim/lib/auth/cimd.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { oauthApplication } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' const logger = createLogger('cimd') @@ -115,7 +116,7 @@ export async function resolveClientMetadata(url: string): Promise return doc }) .catch((err) => { - const message = err instanceof Error ? err.message : String(err) + const message = toError(err).message failureCache.set(url, { error: message, expiresAt: Date.now() + NEGATIVE_CACHE_TTL_MS }) throw err }) diff --git a/apps/sim/lib/billing/calculations/usage-monitor.ts b/apps/sim/lib/billing/calculations/usage-monitor.ts index 01ffca23aff..71a294ba1de 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.ts @@ -7,6 +7,7 @@ import { getUserUsageLimit } from '@/lib/billing/core/usage' import { computeDailyRefreshConsumed } from '@/lib/billing/credits/daily-refresh' import { getPlanTierDollars, isOrgPlan, isPaid } from '@/lib/billing/plan-helpers' import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('UsageMonitor') @@ -190,7 +191,7 @@ export async function checkUsageStatus( // Block execution if we can't determine usage status logger.error('Cannot determine usage status - blocking execution', { userId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return { @@ -369,7 +370,7 @@ export async function checkServerSideUsageLimits( logger.error('Cannot determine usage limits - blocking execution', { userId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return { diff --git a/apps/sim/lib/billing/core/usage-log.ts b/apps/sim/lib/billing/core/usage-log.ts index 4bb2ee75f5a..165c6e3e3f7 100644 --- a/apps/sim/lib/billing/core/usage-log.ts +++ b/apps/sim/lib/billing/core/usage-log.ts @@ -3,6 +3,7 @@ import { usageLog, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, desc, eq, gte, lte, type SQL, sql } from 'drizzle-orm' import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' const logger = createLogger('UsageLog') @@ -304,7 +305,7 @@ export async function getUserUsageLogs( } } catch (error) { logger.error('Failed to get usage logs', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, userId, options, }) diff --git a/apps/sim/lib/chunkers/regex-chunker.ts b/apps/sim/lib/chunkers/regex-chunker.ts index 58c8cb16b91..83b7dfdaee4 100644 --- a/apps/sim/lib/chunkers/regex-chunker.ts +++ b/apps/sim/lib/chunkers/regex-chunker.ts @@ -9,6 +9,7 @@ import { splitAtWordBoundaries, tokensToChars, } from '@/lib/chunkers/utils' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('RegexChunker') @@ -62,9 +63,7 @@ export class RegexChunker { if (error instanceof Error && error.message.includes('catastrophic')) { throw error } - throw new Error( - `Invalid regex pattern "${pattern}": ${error instanceof Error ? error.message : String(error)}` - ) + throw new Error(`Invalid regex pattern "${pattern}": ${toError(error).message}`) } } diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index 536dd8730bd..b998cec31ae 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -4,6 +4,7 @@ import { isPaid } from '@/lib/billing/plan-helpers' import { getToolEntry } from '@/lib/copilot/tool-executor/router' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import { isHosted } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import { createMcpToolId } from '@/lib/mcp/utils' import { trackChatUpload } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { tools } from '@/tools/registry' @@ -89,7 +90,7 @@ export async function buildIntegrationToolSchemas( } catch (error) { reqLogger.warn('Failed to load subscription for copilot tool descriptions', { userId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } @@ -125,7 +126,7 @@ export async function buildIntegrationToolSchemas( : 'Failed to build schema for tool, skipping', { toolId, - error: toolError instanceof Error ? toolError.message : String(toolError), + error: toError(toolError).message, } ) } @@ -136,7 +137,7 @@ export async function buildIntegrationToolSchemas( ? `Failed to build tool schemas [messageId:${messageId}]` : 'Failed to build tool schemas', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, } ) } @@ -221,7 +222,7 @@ export async function buildCopilotRequestPayload( logger.warn('Failed to track chat upload', { filename, chatId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) } } @@ -265,7 +266,7 @@ export async function buildCopilotRequestPayload( ? `Failed to discover MCP tools for copilot [messageId:${userMessageId}]` : 'Failed to discover MCP tools for copilot', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, } ) } diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index 8581621d1f2..468ed4f048c 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -33,6 +33,7 @@ import type { ExecutionContext, OrchestratorResult } from '@/lib/copilot/request import { persistChatResources } from '@/lib/copilot/resources/persistence' import { taskPubSub } from '@/lib/copilot/tasks' import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context' +import { toError } from '@/lib/core/utils/helpers' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' import { getWorkflowById, resolveWorkflowIdForUser } from '@/lib/workflows/utils' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -612,7 +613,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { const userPermissionPromise = workspaceId ? getUserEntityPermissions(authenticatedUserId, 'workspace', workspaceId).catch((error) => { logger.warn('Failed to load user permissions', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, workspaceId, }) return null diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index 2f716f0ec30..b0af547ba66 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -12,6 +12,7 @@ import { import { createLogger } from '@sim/logger' import { and, count, eq, inArray, isNull } from 'drizzle-orm' import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' +import { toError } from '@/lib/core/utils/helpers' import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment' import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace' import { listCustomTools } from '@/lib/workflows/custom-tools/operations' @@ -446,7 +447,7 @@ export async function generateWorkspaceContext( } catch (err) { logger.error('Failed to generate workspace context', { workspaceId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return '## Workspace\n(unavailable)\n\n## Workflows\n(unavailable)\n\n## Knowledge Bases\n(unavailable)\n\n## Tables\n(unavailable)\n\n## Files\n(unavailable)\n\n## Connected Integrations\n(unavailable)' } diff --git a/apps/sim/lib/copilot/persistence/tool-confirm/index.ts b/apps/sim/lib/copilot/persistence/tool-confirm/index.ts index 65f5df9884e..136482be884 100644 --- a/apps/sim/lib/copilot/persistence/tool-confirm/index.ts +++ b/apps/sim/lib/copilot/persistence/tool-confirm/index.ts @@ -8,6 +8,7 @@ import { import { getAsyncToolCalls } from '@/lib/copilot/async-runs/repository' import { MothershipStreamV1ToolOutcome } from '@/lib/copilot/generated/mothership-stream-v1' import { getRedisClient } from '@/lib/core/config/redis' +import { toError } from '@/lib/core/utils/helpers' import { createPubSubChannel } from '@/lib/events/pubsub' const logger = createLogger('CopilotOrchestratorPersistence') @@ -31,7 +32,7 @@ export async function getToolConfirmation( const [row] = await getAsyncToolCalls([toolCallId]).catch((err) => { logger.warn('Failed to fetch async tool calls', { toolCallId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return [] }) @@ -81,7 +82,7 @@ export function publishToolConfirmation(event: AsyncCompletionEnvelope): void { .catch((error) => { logger.warn('Failed to persist tool confirmation in Redis', { toolCallId: event.toolCallId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) }) } else { diff --git a/apps/sim/lib/copilot/request/go/file-preview-adapter.ts b/apps/sim/lib/copilot/request/go/file-preview-adapter.ts index b5f66051319..bc2d17e76ff 100644 --- a/apps/sim/lib/copilot/request/go/file-preview-adapter.ts +++ b/apps/sim/lib/copilot/request/go/file-preview-adapter.ts @@ -25,6 +25,7 @@ import { buildFilePreviewText, loadWorkspaceFileTextForPreview, } from '@/lib/copilot/tools/server/files/file-preview' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('CopilotFilePreviewAdapter') @@ -219,7 +220,7 @@ async function persistFilePreviewSession(session: FilePreviewSession): Promise { logger.warn(`[${requestId}] Failed to create copilot run segment`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) }) } @@ -202,7 +203,7 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS await publisher.close() } catch (error) { logger.warn(`[${requestId}] Failed to flush stream persistence during close`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } unregisterActiveStream(streamId) diff --git a/apps/sim/lib/copilot/request/session/abort.ts b/apps/sim/lib/copilot/request/session/abort.ts index 3502f4a69f8..722f8886bb9 100644 --- a/apps/sim/lib/copilot/request/session/abort.ts +++ b/apps/sim/lib/copilot/request/session/abort.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis' +import { sleep, toError } from '@/lib/core/utils/helpers' import { clearAbortMarker, hasAbortMarker, writeAbortMarker } from './buffer' const logger = createLogger('SessionAbort') @@ -65,7 +66,7 @@ export async function waitForPendingChatStream( logger.warn('Failed to inspect chat stream lock while waiting', { chatId, expectedStreamId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } else if (!localPending) { @@ -75,7 +76,7 @@ export async function waitForPendingChatStream( if (Date.now() >= deadline) { return false } - await new Promise((resolve) => setTimeout(resolve, 200)) + await sleep(200) } } @@ -95,7 +96,7 @@ export async function getPendingChatStreamId(chatId: string): Promise= deadline) { return false } - await new Promise((resolve) => setTimeout(resolve, 200)) + await sleep(200) } } @@ -213,7 +214,7 @@ export function startAbortPoller( logger.warn('Failed to poll stream abort marker', { streamId, ...(requestId ? { requestId } : {}), - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } finally { pollingStreams.delete(streamId) @@ -228,7 +229,7 @@ export async function cleanupAbortMarker(streamId: string): Promise { } catch (error) { logger.warn('Failed to clear stream abort marker during cleanup', { streamId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/lib/copilot/request/session/buffer.ts b/apps/sim/lib/copilot/request/session/buffer.ts index 559d0369e27..58a2f4f280c 100644 --- a/apps/sim/lib/copilot/request/session/buffer.ts +++ b/apps/sim/lib/copilot/request/session/buffer.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { env } from '@/lib/core/config/env' import { getRedisClient } from '@/lib/core/config/redis' +import { sleep, toError } from '@/lib/core/utils/helpers' import { type PersistedStreamEventEnvelope, parsePersistedStreamEventEnvelopeJson, @@ -65,7 +66,7 @@ async function withRedisRetry( for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt++) { const delay = RETRY_DELAYS_MS[attempt] if (delay > 0) { - await new Promise((resolve) => setTimeout(resolve, delay)) + await sleep(delay) } try { @@ -76,7 +77,7 @@ async function withRedisRetry( operation: metadata.operation, streamId: metadata.streamId, attempt: attempt + 1, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -126,7 +127,7 @@ export async function scheduleBufferCleanup( logger.warn('Failed to shorten stream buffer TTL during cleanup', { streamId, ttlSeconds, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/lib/copilot/request/session/file-preview-session.ts b/apps/sim/lib/copilot/request/session/file-preview-session.ts index eda86d4612c..e6dc9df4ae1 100644 --- a/apps/sim/lib/copilot/request/session/file-preview-session.ts +++ b/apps/sim/lib/copilot/request/session/file-preview-session.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getRedisClient } from '@/lib/core/config/redis' +import { sleep, toError } from '@/lib/core/utils/helpers' import { getStreamConfig } from './buffer' import { FILE_PREVIEW_SESSION_SCHEMA_VERSION, @@ -51,7 +52,7 @@ async function withRedisRetry( for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt++) { const delay = RETRY_DELAYS_MS[attempt] if (delay > 0) { - await new Promise((resolve) => setTimeout(resolve, delay)) + await sleep(delay) } try { @@ -62,7 +63,7 @@ async function withRedisRetry( operation: metadata.operation, streamId: metadata.streamId, attempt: attempt + 1, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -142,7 +143,7 @@ export async function readFilePreviewSessions(streamId: string): Promise { - this.lastPersistenceError = error instanceof Error ? error : new Error(String(error)) + this.lastPersistenceError = toError(error) logger.warn('Failed to persist stream envelope batch', { streamId: this.streamId, requestId: this.requestId, batchSize: batch.length, firstSeq: batch[0]?.seq, lastSeq: batch[batch.length - 1]?.seq, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) }) } diff --git a/apps/sim/lib/copilot/request/subagent.ts b/apps/sim/lib/copilot/request/subagent.ts index d9403094698..6e3c143f343 100644 --- a/apps/sim/lib/copilot/request/subagent.ts +++ b/apps/sim/lib/copilot/request/subagent.ts @@ -18,6 +18,7 @@ import type { import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context' import { env } from '@/lib/core/config/env' import { isHosted } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' import { getWorkflowById } from '@/lib/workflows/utils' @@ -78,7 +79,7 @@ export async function orchestrateSubagentStream( logger.warn('Failed to generate workspace context for subagent request', { agentId, workspaceId: resolvedWorkspaceId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/lib/copilot/request/tools/executor.ts b/apps/sim/lib/copilot/request/tools/executor.ts index bc1cb26cde9..640f013c986 100644 --- a/apps/sim/lib/copilot/request/tools/executor.ts +++ b/apps/sim/lib/copilot/request/tools/executor.ts @@ -40,6 +40,7 @@ import { type ToolCallState, } from '@/lib/copilot/request/types' import { ensureHandlersRegistered, executeTool } from '@/lib/copilot/tool-executor' +import { toError } from '@/lib/core/utils/helpers' export { waitForToolCompletion } from '@/lib/copilot/request/tools/client' @@ -191,7 +192,7 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool status', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) publishTerminalToolConfirmation({ @@ -212,13 +213,13 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool row before execution', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) await markAsyncToolRunning(toolCall.id, 'sim-stream').catch((err) => { logger.warn('Failed to mark async tool running', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) @@ -289,7 +290,7 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool status', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) publishTerminalToolConfirmation({ @@ -316,7 +317,7 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool status', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) publishTerminalToolConfirmation({ @@ -340,7 +341,7 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool status', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) publishTerminalToolConfirmation({ @@ -364,7 +365,7 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool status', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) publishTerminalToolConfirmation({ @@ -438,7 +439,7 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool completion', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) publishTerminalToolConfirmation({ @@ -499,7 +500,7 @@ export async function executeToolAndReport( ...(terminalData !== undefined ? { data: terminalData } : {}), }) } catch (error) { - const thrownMessage = error instanceof Error ? error.message : String(error) + const thrownMessage = toError(error).message if (abortRequested(context, execContext, options)) { markToolCallCancelled('Request aborted during tool execution') markToolResultSeen(toolCall.id) @@ -511,7 +512,7 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool status', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) publishTerminalToolConfirmation({ @@ -547,7 +548,7 @@ export async function executeToolAndReport( }).catch((err) => { logger.warn('Failed to persist async tool error', { toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) publishTerminalToolConfirmation({ diff --git a/apps/sim/lib/copilot/request/tools/files.ts b/apps/sim/lib/copilot/request/tools/files.ts index 51c4e307c8c..077cd3b58dd 100644 --- a/apps/sim/lib/copilot/request/tools/files.ts +++ b/apps/sim/lib/copilot/request/tools/files.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { FunctionExecute, UserTable } from '@/lib/copilot/generated/tool-catalog-v1' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' const logger = createLogger('CopilotToolResultFiles') @@ -201,7 +202,7 @@ export async function maybeWriteOutputToFile( resources: [{ type: 'file', id: uploaded.id, title: fileName }], } } catch (err) { - const message = err instanceof Error ? err.message : String(err) + const message = toError(err).message logger.warn('Failed to write tool output to file', { toolName, outputPath, diff --git a/apps/sim/lib/copilot/request/tools/resources.ts b/apps/sim/lib/copilot/request/tools/resources.ts index b14f0caf79e..92458d1628c 100644 --- a/apps/sim/lib/copilot/request/tools/resources.ts +++ b/apps/sim/lib/copilot/request/tools/resources.ts @@ -12,6 +12,7 @@ import { persistChatResources, removeChatResources, } from '@/lib/copilot/resources/persistence' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('CopilotResourceEffects') @@ -38,7 +39,7 @@ export async function handleResourceSideEffects( removeChatResources(chatId, deleted).catch((err) => { logger.warn('Failed to remove chat resources after deletion', { chatId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) @@ -72,7 +73,7 @@ export async function handleResourceSideEffects( persistChatResources(chatId, resources).catch((err) => { logger.warn('Failed to persist chat resources', { chatId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) diff --git a/apps/sim/lib/copilot/request/tools/tables.ts b/apps/sim/lib/copilot/request/tools/tables.ts index 89e0a5c19f0..f6cc4c5ea4f 100644 --- a/apps/sim/lib/copilot/request/tools/tables.ts +++ b/apps/sim/lib/copilot/request/tools/tables.ts @@ -5,6 +5,7 @@ import { parse as csvParse } from 'csv-parse/sync' import { eq } from 'drizzle-orm' import { FunctionExecute, Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { getTableById } from '@/lib/table/service' const logger = createLogger('CopilotToolResultTables') @@ -117,11 +118,11 @@ export async function maybeWriteOutputToTable( logger.warn('Failed to write tool output to table', { toolName, outputTable, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, - error: `Failed to write to table: ${err instanceof Error ? err.message : String(err)}`, + error: `Failed to write to table: ${toError(err).message}`, } } } @@ -238,11 +239,11 @@ export async function maybeWriteReadCsvToTable( logger.warn('Failed to write read output to table', { toolName, outputTable, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, - error: `Failed to import into table: ${err instanceof Error ? err.message : String(err)}`, + error: `Failed to import into table: ${toError(err).message}`, } } } diff --git a/apps/sim/lib/copilot/resources/persistence.ts b/apps/sim/lib/copilot/resources/persistence.ts index 9ffc724c1ab..f94b49c7749 100644 --- a/apps/sim/lib/copilot/resources/persistence.ts +++ b/apps/sim/lib/copilot/resources/persistence.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq, sql } from 'drizzle-orm' +import { toError } from '@/lib/core/utils/helpers' import type { MothershipResource } from './types' export { @@ -64,7 +65,7 @@ export async function persistChatResources( } catch (err) { logger.warn('Failed to persist chat resources', { chatId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) } } @@ -97,7 +98,7 @@ export async function removeChatResources(chatId: string, toRemove: ChatResource } catch (err) { logger.warn('Failed to remove chat resources', { chatId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) } } diff --git a/apps/sim/lib/copilot/tool-executor/executor.ts b/apps/sim/lib/copilot/tool-executor/executor.ts index f35a1116ee3..cc904776335 100644 --- a/apps/sim/lib/copilot/tool-executor/executor.ts +++ b/apps/sim/lib/copilot/tool-executor/executor.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { executeTool as executeAppTool } from '@/tools' import { isKnownTool, isSimExecuted } from './router' import type { @@ -58,7 +59,7 @@ export async function executeTool( try { return await handler(params, context) } catch (error) { - const message = error instanceof Error ? error.message : String(error) + const message = toError(error).message logger.error('Tool execution failed', { toolId, error: message, diff --git a/apps/sim/lib/copilot/tools/client/run-tool-execution.ts b/apps/sim/lib/copilot/tools/client/run-tool-execution.ts index 860dc7f0184..ad93eb2c4dc 100644 --- a/apps/sim/lib/copilot/tools/client/run-tool-execution.ts +++ b/apps/sim/lib/copilot/tools/client/run-tool-execution.ts @@ -7,6 +7,7 @@ import { RunFromBlock, RunWorkflowUntilBlock, } from '@/lib/copilot/generated/tool-catalog-v1' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { executeWorkflowWithFullLogging } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils' import { useExecutionStore } from '@/stores/execution/store' @@ -140,7 +141,7 @@ export function executeRunToolOnClient( logger.error('[RunTool] Unhandled error in client-side run tool execution', { toolCallId, toolName, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) } @@ -389,7 +390,7 @@ async function doExecuteRunTool( toolName, }) } else { - const msg = err instanceof Error ? err.message : String(err) + const msg = toError(err).message logger.error('[RunTool] Workflow execution threw', { toolCallId, toolName, error: msg }) await reportCompletion(toolCallId, MothershipStreamV1ToolOutcome.error, msg) } @@ -499,7 +500,7 @@ async function reportCompletion( } catch (err) { logger.error('[RunTool] reportCompletion error', { toolCallId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) } } diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts index 8c990213724..a661e71941e 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts @@ -3,6 +3,7 @@ import { chat, workflowMcpServer, workflowMcpTool } from '@sim/db/schema' import { and, eq, isNull } from 'drizzle-orm' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { getBaseUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { mcpPubSub } from '@/lib/mcp/pubsub' @@ -198,7 +199,7 @@ export async function executeDeployApi( }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -440,7 +441,7 @@ export async function executeDeployChat( }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -712,7 +713,7 @@ export async function executeDeployMcp( }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -764,6 +765,6 @@ export async function executeRedeploy( }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts b/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts index 771089f8cae..b33afbbfa96 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts @@ -9,6 +9,7 @@ import { import { and, eq, inArray, isNull } from 'drizzle-orm' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { mcpPubSub } from '@/lib/mcp/pubsub' import { generateParameterSchemaForWorkflow } from '@/lib/mcp/workflow-mcp-sync' @@ -111,7 +112,7 @@ export async function executeCheckDeploymentStatus( output: { isDeployed, api: apiDetails, chat: chatDetails, mcp: mcpDetails }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -171,7 +172,7 @@ export async function executeListWorkspaceMcpServers( return { success: true, output: { servers: serversWithToolNames, count: servers.length } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -265,7 +266,7 @@ export async function executeCreateWorkspaceMcpServer( return { success: true, output: { server, addedTools } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -328,7 +329,7 @@ export async function executeUpdateWorkspaceMcpServer( return { success: true, output: { serverId, ...updates, updatedAt: undefined } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -374,7 +375,7 @@ export async function executeDeleteWorkspaceMcpServer( return { success: true, output: { serverId, name: existing.name, deleted: true } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -411,7 +412,7 @@ export async function executeGetDeploymentVersion( return { success: true, output: { version, deployedState: row.state } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -453,6 +454,6 @@ export async function executeRevertToVersion( }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } diff --git a/apps/sim/lib/copilot/tools/handlers/jobs.ts b/apps/sim/lib/copilot/tools/handlers/jobs.ts index f271e19ddea..30271f8e33d 100644 --- a/apps/sim/lib/copilot/tools/handlers/jobs.ts +++ b/apps/sim/lib/copilot/tools/handlers/jobs.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { parseCronToHumanReadable, validateCronExpression } from '@/lib/workflows/schedules/utils' @@ -59,7 +60,7 @@ export async function executeCreateJob( } catch (err) { logger.warn('Failed to look up chat title for job', { chatId: context.chatId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) } } @@ -174,7 +175,7 @@ export async function executeCreateJob( } } catch (err) { logger.error('Failed to create job', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, error: 'Failed to create job' } } @@ -271,7 +272,7 @@ export async function executeManageJob( } } catch (err) { logger.error('Failed to list jobs', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, error: 'Failed to list jobs' } } @@ -317,7 +318,7 @@ export async function executeManageJob( } } catch (err) { logger.error('Failed to get job', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, error: 'Failed to get job' } } @@ -417,7 +418,7 @@ export async function executeManageJob( } } catch (err) { logger.error('Failed to update job', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, error: 'Failed to update job' } } @@ -467,7 +468,7 @@ export async function executeManageJob( } } catch (err) { logger.error('Failed to delete job', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, error: 'Failed to delete job' } } @@ -547,7 +548,7 @@ export async function executeCompleteJob( } } catch (err) { logger.error('Failed to complete job', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, error: 'Failed to complete job' } } @@ -597,7 +598,7 @@ export async function executeUpdateJobHistory( } } catch (err) { logger.error('Failed to update job history', { - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) return { success: false, error: 'Failed to update job history' } } diff --git a/apps/sim/lib/copilot/tools/handlers/management/manage-credential.ts b/apps/sim/lib/copilot/tools/handlers/management/manage-credential.ts index da775526d4f..f2d3d75c4b8 100644 --- a/apps/sim/lib/copilot/tools/handlers/management/manage-credential.ts +++ b/apps/sim/lib/copilot/tools/handlers/management/manage-credential.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { credential } from '@sim/db/schema' import { eq } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' export function executeManageCredential( rawParams: Record, @@ -77,7 +78,7 @@ export function executeManageCredential( } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } })() } diff --git a/apps/sim/lib/copilot/tools/handlers/management/manage-custom-tool.ts b/apps/sim/lib/copilot/tools/handlers/management/manage-custom-tool.ts index b67eb3cf146..13c6f3fa757 100644 --- a/apps/sim/lib/copilot/tools/handlers/management/manage-custom-tool.ts +++ b/apps/sim/lib/copilot/tools/handlers/management/manage-custom-tool.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { deleteCustomTool, getCustomToolById, @@ -206,7 +207,7 @@ export async function executeManageCustomTool( operation, workspaceId, userId: context.userId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, } ) return { diff --git a/apps/sim/lib/copilot/tools/handlers/management/manage-mcp-tool.ts b/apps/sim/lib/copilot/tools/handlers/management/manage-mcp-tool.ts index be7b327bdbc..366ba5b511f 100644 --- a/apps/sim/lib/copilot/tools/handlers/management/manage-mcp-tool.ts +++ b/apps/sim/lib/copilot/tools/handlers/management/manage-mcp-tool.ts @@ -3,6 +3,7 @@ import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { validateMcpDomain } from '@/lib/mcp/domain-check' import { mcpService } from '@/lib/mcp/service' import { generateMcpServerId } from '@/lib/mcp/utils' @@ -235,7 +236,7 @@ export async function executeManageMcpTool( { operation, workspaceId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, } ) return { diff --git a/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts b/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts index a1e5ed7de9e..5b8b59acda6 100644 --- a/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts +++ b/apps/sim/lib/copilot/tools/handlers/management/manage-skill.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { deleteSkill, listSkills, upsertSkills } from '@/lib/workflows/skills/operations' const logger = createLogger('CopilotToolExecutor') @@ -162,7 +163,7 @@ export async function executeManageSkill( { operation, workspaceId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, } ) return { diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts index 0f59a81a6fa..6d1fcbeb392 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts @@ -5,6 +5,7 @@ import { and, eq, isNull } from 'drizzle-orm' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { findMothershipUploadRowByChatAndName } from '@/lib/copilot/tools/handlers/upload-file-reader' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { getServePathPrefix } from '@/lib/uploads' import { downloadWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' @@ -219,7 +220,7 @@ export async function executeMaterializeFile( fileName, operation, chatId: context.chatId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) failed.push({ fileName, diff --git a/apps/sim/lib/copilot/tools/handlers/oauth.ts b/apps/sim/lib/copilot/tools/handlers/oauth.ts index 7ece3645fce..4c7a7829116 100644 --- a/apps/sim/lib/copilot/tools/handlers/oauth.ts +++ b/apps/sim/lib/copilot/tools/handlers/oauth.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { pendingCredentialDraft, user } from '@sim/db/schema' import { and, eq, lt } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { getBaseUrl } from '@/lib/core/utils/urls' import { getAllOAuthServices } from '@/lib/oauth/utils' @@ -36,11 +37,11 @@ export async function executeOAuthGetAuthLink( : `${baseUrl}/workspace` return { success: false, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, output: { message: `Could not generate a direct OAuth link for ${providerName}. Connect manually from the workspace.`, oauth_url: workspaceUrl, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }, } } diff --git a/apps/sim/lib/copilot/tools/handlers/restore-resource.ts b/apps/sim/lib/copilot/tools/handlers/restore-resource.ts index 3954d08fbfe..893a76662cf 100644 --- a/apps/sim/lib/copilot/tools/handlers/restore-resource.ts +++ b/apps/sim/lib/copilot/tools/handlers/restore-resource.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { restoreKnowledgeBase } from '@/lib/knowledge/service' import { getTableById, restoreTable } from '@/lib/table/service' @@ -101,6 +102,6 @@ export async function executeRestoreResource( return { success: false, error: `Unsupported type: ${type}` } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } diff --git a/apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts b/apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts index cf40f13584d..d0ae21ac07a 100644 --- a/apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts +++ b/apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import { type FileReadResult, readFileRecord } from '@/lib/copilot/vfs/file-reader' import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' +import { toError } from '@/lib/core/utils/helpers' import { getServePathPrefix } from '@/lib/uploads' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' @@ -86,7 +87,7 @@ export async function listChatUploads(chatId: string): Promise 0, output: { moved, failed, folderId } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -566,7 +567,7 @@ export async function executeMoveFolder( return { success: true, output: { folderId, parentId } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -663,7 +664,7 @@ export async function executeGenerateApiKey( }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -764,7 +765,7 @@ export async function executeUpdateWorkflow( output: { workflowId, ...updates }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -895,7 +896,7 @@ export async function executeSetBlockEnabled( }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -937,7 +938,7 @@ export async function executeDeleteWorkflow( output: { deleted, failed }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -983,7 +984,7 @@ export async function executeDeleteFolder( return { success: deleted.length > 0, output: { deleted, failed } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -1011,7 +1012,7 @@ export async function executeRenameFolder( return { success: true, output: { folderId, name } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts b/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts index 03585eb82b3..71d85576448 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts @@ -1,5 +1,6 @@ import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { formatNormalizedWorkflowForCopilot } from '@/lib/copilot/tools/shared/workflow-utils' +import { toError } from '@/lib/core/utils/helpers' import { mcpService } from '@/lib/mcp/service' import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace' import { getEffectiveBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs' @@ -33,7 +34,7 @@ export async function executeListUserWorkspaces( return { success: true, output: { workspaces } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -59,7 +60,7 @@ export async function executeListFolders( }, } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -153,7 +154,7 @@ export async function executeGetWorkflowData( return { success: false, error: `Unknown data_type: ${dataType}` } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -238,7 +239,7 @@ export async function executeGetBlockOutputs( const payload = { blocks: results, variables } return { success: true, output: payload } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -390,7 +391,7 @@ export async function executeGetBlockUpstreamReferences( const payload = { results } return { success: true, output: payload } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } @@ -483,6 +484,6 @@ export async function executeGetDeployedWorkflowState( } } } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + return { success: false, error: toError(error).message } } } diff --git a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts index d9defa43e13..98cc122bc0e 100644 --- a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts +++ b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import type { ToolExecutionResult, ToolHandler } from '@/lib/copilot/tool-executor/types' import { routeExecution } from '@/lib/copilot/tools/server/router' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('ServerToolAdapter') @@ -35,7 +36,7 @@ export function createServerToolHandler(toolId: string): ToolHandler { } return { success: true, output: result } } catch (error) { - const message = error instanceof Error ? error.message : String(error) + const message = toError(error).message logger.error('Server tool execution failed', { toolId, error: message, diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index 209ff1673f3..362d7728bac 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -5,6 +5,7 @@ import { z } from 'zod' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { getAllowedIntegrationsFromEnv, isHosted } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import { getServiceAccountProviderForProviderId } from '@/lib/oauth/utils' import { registry as blockRegistry } from '@/blocks/registry' import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types' @@ -313,7 +314,7 @@ export const getBlocksMetadataServerTool: BaseServerTool< } } catch (error) { logger.warn('Failed to read YAML documentation file', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } @@ -1000,7 +1001,7 @@ function resolveToolIdForOperation(blockConfig: BlockConfig, opId: string): stri } catch (error) { const toolLogger = createLogger('GetBlocksMetadataServerTool') toolLogger.warn('Failed to resolve tool ID for operation', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } return undefined diff --git a/apps/sim/lib/copilot/tools/server/files/edit-content.ts b/apps/sim/lib/copilot/tools/server/files/edit-content.ts index 114c0ae7ef7..692ac4ef547 100644 --- a/apps/sim/lib/copilot/tools/server/files/edit-content.ts +++ b/apps/sim/lib/copilot/tools/server/files/edit-content.ts @@ -4,6 +4,7 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { toError } from '@/lib/core/utils/helpers' import { generateDocxFromCode, generatePdfFromCode, @@ -242,7 +243,7 @@ export const editContentServerTool: BaseServerTool( for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt++) { const delay = RETRY_DELAYS_MS[attempt] if (delay > 0) { - await new Promise((resolve) => setTimeout(resolve, delay)) + await sleep(delay) } try { @@ -91,7 +92,7 @@ async function withRedisRetry( operation, workspaceId, attempt: attempt + 1, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -110,7 +111,7 @@ function parseIntent(raw: string | null | undefined): PendingFileIntent | undefi return isStale(parsed) ? undefined : parsed } catch (error) { logger.warn('Failed to parse file intent', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return undefined } diff --git a/apps/sim/lib/copilot/tools/server/files/file-preview.ts b/apps/sim/lib/copilot/tools/server/files/file-preview.ts index fd783321b3f..e70cdab8c5d 100644 --- a/apps/sim/lib/copilot/tools/server/files/file-preview.ts +++ b/apps/sim/lib/copilot/tools/server/files/file-preview.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { downloadWorkspaceFile, getWorkspaceFile, @@ -153,7 +154,7 @@ export async function loadWorkspaceFileTextForPreview( logger.warn('Failed to load workspace file text for preview', { workspaceId, fileId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return undefined } diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index a9ae344192c..9678939ae69 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -5,6 +5,7 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { toError } from '@/lib/core/utils/helpers' import { generateDocxFromCode, generatePdfFromCode, @@ -203,7 +204,7 @@ export const workspaceFileServerTool: BaseServerTool { logger.error('Background document processing failed', { documentId: doc.id, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) }) diff --git a/apps/sim/lib/copilot/tools/server/other/search-online.ts b/apps/sim/lib/copilot/tools/server/other/search-online.ts index f9a50956f77..8a7dfc337a2 100644 --- a/apps/sim/lib/copilot/tools/server/other/search-online.ts +++ b/apps/sim/lib/copilot/tools/server/other/search-online.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { SearchOnline } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { env } from '@/lib/core/config/env' +import { toError } from '@/lib/core/utils/helpers' import { executeTool } from '@/tools' interface OnlineSearchParams { @@ -84,7 +85,7 @@ export const searchOnlineServerTool: BaseServerTool return { success: false, message: `Unknown operation: ${operation}` } } } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - const cause = - error instanceof Error && error.cause - ? error.cause instanceof Error - ? error.cause.message - : String(error.cause) - : undefined + const errorMessage = toError(error).message + const cause = error instanceof Error && error.cause ? toError(error.cause).message : undefined logger.error('Table operation failed', { operation, error: errorMessage, diff --git a/apps/sim/lib/copilot/tools/server/user/get-credentials.ts b/apps/sim/lib/copilot/tools/server/user/get-credentials.ts index 9f0c8b41193..b7c329efad1 100644 --- a/apps/sim/lib/copilot/tools/server/user/get-credentials.ts +++ b/apps/sim/lib/copilot/tools/server/user/get-credentials.ts @@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm' import { jwtDecode } from 'jwt-decode' import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' +import { toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' import { getAllOAuthServices } from '@/lib/oauth' @@ -91,7 +92,7 @@ export const getCredentialsServerTool: BaseServerTool displayName = decoded.email || decoded.name || '' } catch (error) { logger.warn('Failed to decode JWT id token', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -113,7 +114,7 @@ export const getCredentialsServerTool: BaseServerTool accessToken = refreshedToken || accessToken } catch (error) { logger.warn('Failed to refresh OAuth access token', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } connectedCredentials.push({ diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts index e5a414fc62c..7c9e09328b6 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts @@ -9,6 +9,7 @@ import { type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' import { env } from '@/lib/core/config/env' +import { toError } from '@/lib/core/utils/helpers' import { getSocketServerUrl } from '@/lib/core/utils/urls' import { applyTargetedLayout, @@ -155,7 +156,7 @@ export const editWorkflowServerTool: BaseServerTool validationErrors.push(...selectorErrors) } catch (error) { logger.warn('Selector ID validation failed', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -249,7 +250,7 @@ export const editWorkflowServerTool: BaseServerTool } catch (error) { logger.warn('Targeted autolayout failed, using default positions', { workflowId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/lib/copilot/validation/selector-validator.ts b/apps/sim/lib/copilot/validation/selector-validator.ts index 65123904342..f4a174c3699 100644 --- a/apps/sim/lib/copilot/validation/selector-validator.ts +++ b/apps/sim/lib/copilot/validation/selector-validator.ts @@ -10,6 +10,7 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull, or } from 'drizzle-orm' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('SelectorValidator') @@ -253,7 +254,7 @@ export async function validateSelectorIds( } catch (error) { // On DB error, skip validation rather than failing the edit logger.error(`Failed to validate selector IDs for type "${selectorType}"`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, selectorType, idCount: idsArray.length, }) diff --git a/apps/sim/lib/copilot/vfs/file-reader.ts b/apps/sim/lib/copilot/vfs/file-reader.ts index 00f2e2dc55e..76e4a86e3eb 100644 --- a/apps/sim/lib/copilot/vfs/file-reader.ts +++ b/apps/sim/lib/copilot/vfs/file-reader.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { downloadWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { isImageFileType } from '@/lib/uploads/utils/file-utils' @@ -109,7 +110,7 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise= MAX_PING_FAILURES) { diff --git a/apps/sim/lib/core/idempotency/cleanup.ts b/apps/sim/lib/core/idempotency/cleanup.ts index 1569bfd14c1..de8929d1524 100644 --- a/apps/sim/lib/core/idempotency/cleanup.ts +++ b/apps/sim/lib/core/idempotency/cleanup.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { idempotencyKey } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, count, inArray, like, lt, max, min, sql } from 'drizzle-orm' +import { sleep } from '@/lib/core/utils/helpers' const logger = createLogger('IdempotencyCleanup') @@ -97,7 +98,7 @@ export async function cleanupExpiredIdempotencyKeys( } else { logger.info(`Deleted batch ${batchCount}: ${deletedCount} expired idempotency keys`) - await new Promise((resolve) => setTimeout(resolve, 100)) + await sleep(100) } } catch (batchError) { const errorMessage = diff --git a/apps/sim/lib/core/idempotency/service.ts b/apps/sim/lib/core/idempotency/service.ts index 5f19d4c89de..96ed86a3bb3 100644 --- a/apps/sim/lib/core/idempotency/service.ts +++ b/apps/sim/lib/core/idempotency/service.ts @@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm' import { getRedisClient } from '@/lib/core/config/redis' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { getStorageMethod, type StorageMethod } from '@/lib/core/storage' +import { sleep } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { extractProviderIdentifierFromBody } from '@/lib/webhooks/providers' @@ -293,7 +294,7 @@ export class IdempotencyService { throw new Error(currentResult.error || 'Previous operation failed') } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) } throw new Error(`Timeout waiting for idempotency operation to complete: ${normalizedKey}`) diff --git a/apps/sim/lib/core/rate-limiter/rate-limiter.ts b/apps/sim/lib/core/rate-limiter/rate-limiter.ts index 6550496cb91..50186ce9755 100644 --- a/apps/sim/lib/core/rate-limiter/rate-limiter.ts +++ b/apps/sim/lib/core/rate-limiter/rate-limiter.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { isOrgPlan } from '@/lib/billing/plan-helpers' +import { toError } from '@/lib/core/utils/helpers' import { createStorageAdapter, type RateLimitStorageAdapter } from './storage' import { getRateLimit, @@ -101,7 +102,7 @@ export class RateLimiter { } } catch (error) { logger.error('Rate limit storage error - failing open (allowing request)', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, userId, triggerType, isAsync, @@ -147,7 +148,7 @@ export class RateLimiter { } } catch (error) { logger.error('Error getting rate limit status - returning default config', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, userId, triggerType, isAsync, @@ -181,7 +182,7 @@ export class RateLimiter { } } catch (error) { logger.error('Rate limit storage error - failing open (allowing request)', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, storageKey, }) return { diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index 95458150b7d..7dcb89093c5 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -6,6 +6,7 @@ import { createLogger } from '@sim/logger' import * as ipaddr from 'ipaddr.js' import { isHosted } from '@/lib/core/config/feature-flags' import { type ValidationResult, validateExternalUrl } from '@/lib/core/security/input-validation' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('InputValidation') @@ -111,7 +112,7 @@ export async function validateUrlWithDNS( logger.warn('DNS lookup failed for URL', { paramName, hostname, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return { isValid: false, @@ -175,7 +176,7 @@ export async function validateDatabaseHost( logger.warn('DNS lookup failed for database host', { paramName, hostname: host, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return { isValid: false, diff --git a/apps/sim/lib/core/security/input-validation.test.ts b/apps/sim/lib/core/security/input-validation.test.ts index c01f1cbdd50..f08578494da 100644 --- a/apps/sim/lib/core/security/input-validation.test.ts +++ b/apps/sim/lib/core/security/input-validation.test.ts @@ -21,6 +21,7 @@ import { validatePathSegment, validateProxyUrl, validateS3BucketName, + validateSupabaseProjectId, } from '@/lib/core/security/input-validation' import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import { sanitizeForLogging } from '@/lib/core/security/redaction' @@ -1719,4 +1720,123 @@ describe('validateMondayColumnId', () => { expect(result.isValid).toBe(false) }) }) + + describe('validateSupabaseProjectId', () => { + describe('valid inputs', () => { + it.concurrent('should accept a typical 20-char lowercase alphanumeric project ID', () => { + const result = validateSupabaseProjectId('jdrkgepadsdopsntdlom') + expect(result.isValid).toBe(true) + expect(result.sanitized).toBe('jdrkgepadsdopsntdlom') + }) + + it.concurrent('should accept project IDs with digits', () => { + const result = validateSupabaseProjectId('abc123def456ghi789jk') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept IDs at the minimum length boundary (10)', () => { + const result = validateSupabaseProjectId('abcdefghij') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept IDs at the maximum length boundary (40)', () => { + const result = validateSupabaseProjectId('a'.repeat(40)) + expect(result.isValid).toBe(true) + }) + }) + + describe('SSRF attack vectors', () => { + it.concurrent('should reject fragment injection (#)', () => { + const result = validateSupabaseProjectId('evil#attacker.com') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject @ for authority injection', () => { + const result = validateSupabaseProjectId('evil@attacker.com') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject path traversal with slashes', () => { + const result = validateSupabaseProjectId('evil/../../etc/passwd') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject dots (subdomain manipulation)', () => { + const result = validateSupabaseProjectId('evil.attacker.com') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject backslashes', () => { + const result = validateSupabaseProjectId('evil\\path') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject colons (port injection)', () => { + const result = validateSupabaseProjectId('evil:8080') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject URL-encoded characters', () => { + const result = validateSupabaseProjectId('evil%23attacker') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject spaces', () => { + const result = validateSupabaseProjectId('evil host') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject newlines (header injection)', () => { + const result = validateSupabaseProjectId('evil\r\nHost: attacker.com') + expect(result.isValid).toBe(false) + }) + }) + + describe('invalid formats', () => { + it.concurrent('should reject null', () => { + const result = validateSupabaseProjectId(null) + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject undefined', () => { + const result = validateSupabaseProjectId(undefined) + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject empty string', () => { + const result = validateSupabaseProjectId('') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject uppercase letters', () => { + const result = validateSupabaseProjectId('JDRKGEPADSDOPSNTDLOM') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject mixed case', () => { + const result = validateSupabaseProjectId('jdrkGEPadsdOPSntdlom') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject hyphens', () => { + const result = validateSupabaseProjectId('jdrk-gepa-dsdo-psnt') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject underscores', () => { + const result = validateSupabaseProjectId('jdrk_gepa_dsdo_psnt') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject IDs shorter than 10 characters', () => { + const result = validateSupabaseProjectId('abcdefghi') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject IDs longer than 40 characters', () => { + const result = validateSupabaseProjectId('a'.repeat(41)) + expect(result.isValid).toBe(false) + }) + }) + }) }) diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 8515f1ecd0d..42cb49070e3 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1400,6 +1400,55 @@ export function validateMondayColumnId( return { isValid: true, sanitized: value } } +/** + * Validates a Supabase project ID. + * + * Supabase project IDs are 20-character lowercase alphanumeric strings + * (e.g. "jdrkgepadsdopsntdlom"). This validator ensures the value cannot + * contain URL-fragment (`#`), path-separator, or other characters that + * would let an attacker break out of the `*.supabase.co` domain when the + * ID is interpolated into a URL template. + * + * @param value - The project ID to validate + * @param paramName - Name of the parameter for error messages + * @returns ValidationResult + */ +export function validateSupabaseProjectId( + value: string | null | undefined, + paramName = 'projectId' +): ValidationResult { + if (value === null || value === undefined || value === '') { + return { + isValid: false, + error: `${paramName} is required`, + } + } + + if (!/^[a-z0-9]+$/.test(value)) { + logger.warn('Invalid Supabase project ID format', { + paramName, + value: value.substring(0, 50), + }) + return { + isValid: false, + error: `${paramName} must contain only lowercase alphanumeric characters`, + } + } + + if (value.length < 10 || value.length > 40) { + logger.warn('Supabase project ID length invalid', { + paramName, + length: value.length, + }) + return { + isValid: false, + error: `${paramName} must be between 10 and 40 characters`, + } + } + + return { isValid: true, sanitized: value } +} + export function isMicrosoftContentUrl(url: string): boolean { let hostname: string try { diff --git a/apps/sim/lib/core/telemetry.ts b/apps/sim/lib/core/telemetry.ts index 5bb0c6227b7..e9508f0e247 100644 --- a/apps/sim/lib/core/telemetry.ts +++ b/apps/sim/lib/core/telemetry.ts @@ -18,6 +18,7 @@ import { context, type Span, SpanStatusCode, trace } from '@opentelemetry/api' import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { TraceSpan } from '@/lib/logs/types' /** @@ -418,7 +419,7 @@ export async function traceBlockExecution( code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Block execution failed', }) - span.recordException(error instanceof Error ? error : new Error(String(error))) + span.recordException(toError(error)) throw error } finally { span.end() diff --git a/apps/sim/lib/core/utils/asserts.ts b/apps/sim/lib/core/utils/asserts.ts new file mode 100644 index 00000000000..1d463652f1a --- /dev/null +++ b/apps/sim/lib/core/utils/asserts.ts @@ -0,0 +1,17 @@ +/** + * Asserts that a condition is truthy, throwing an Error if it is not. + * Use for invariants that should never be violated at runtime. + */ +export function invariant(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message) + } +} + +/** + * Asserts that a value is `never`, useful for exhaustive switch/if-else checks. + * TypeScript will error at compile time if a case is unhandled. + */ +export function assertNever(value: never, message?: string): never { + throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`) +} diff --git a/apps/sim/lib/core/utils/helpers.ts b/apps/sim/lib/core/utils/helpers.ts new file mode 100644 index 00000000000..40c1a4a6f58 --- /dev/null +++ b/apps/sim/lib/core/utils/helpers.ts @@ -0,0 +1,39 @@ +/** + * Returns a promise that resolves after the specified duration. + * Replaces the common `new Promise(resolve => setTimeout(resolve, ms))` pattern. + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +/** + * Parses a JSON string, returning a fallback value on failure instead of throwing. + * Replaces the common `try { JSON.parse(str) } catch { return default }` pattern. + */ +export function safeJsonParse(value: string): T | undefined +export function safeJsonParse(value: string, fallback: T): T +export function safeJsonParse(value: string, fallback?: T): T | undefined { + try { + return JSON.parse(value) as T + } catch { + return fallback + } +} + +/** + * Type-safe filter predicate that removes null and undefined values. + * Fixes the common `.filter(Boolean)` pattern which doesn't narrow types in TypeScript. + */ +export function isNonNull(value: T | null | undefined): value is T { + return value != null +} + +/** + * Normalizes an unknown caught value into an Error instance. + * Replaces the common `e instanceof Error ? e : new Error(String(e))` pattern in catch clauses. + */ +export function toError(value: unknown): Error { + if (value instanceof Error) return value + if (typeof value === 'string') return new Error(value) + return new Error(String(value)) +} diff --git a/apps/sim/lib/execution/buffered-stream.ts b/apps/sim/lib/execution/buffered-stream.ts index f1b413b6f96..a811faf1d28 100644 --- a/apps/sim/lib/execution/buffered-stream.ts +++ b/apps/sim/lib/execution/buffered-stream.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { sleep, toError } from '@/lib/core/utils/helpers' import { type ExecutionStreamStatus, getExecutionMeta, @@ -69,7 +70,7 @@ export function createBufferedExecutionStream( return } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) if (closed) { return } @@ -93,7 +94,7 @@ export function createBufferedExecutionStream( } catch (error) { logger.error('Buffered execution stream failed', { executionId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) if (!closed) { diff --git a/apps/sim/lib/execution/doc-vm.ts b/apps/sim/lib/execution/doc-vm.ts index 2620f2e47cf..e2a7cc4c464 100644 --- a/apps/sim/lib/execution/doc-vm.ts +++ b/apps/sim/lib/execution/doc-vm.ts @@ -11,6 +11,7 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { downloadWorkspaceFile, getWorkspaceFile, @@ -96,7 +97,7 @@ export async function generateDocumentFromCode( env: { PATH: process.env.PATH ?? '' } as unknown as NodeJS.ProcessEnv, }) } catch (err) { - done(err instanceof Error ? err : new Error(String(err))) + done(toError(err)) return } @@ -158,7 +159,7 @@ export async function generateDocumentFromCode( handleFileRequest(proc!, workspaceId, msg).catch((err) => { logger.error(`Failed to handle file request from ${format} worker`, { fileId: msg.fileId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) if (proc && !settled) { try { diff --git a/apps/sim/lib/execution/event-buffer.ts b/apps/sim/lib/execution/event-buffer.ts index 81373ccf42e..ccf7bab9d2b 100644 --- a/apps/sim/lib/execution/event-buffer.ts +++ b/apps/sim/lib/execution/event-buffer.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getRedisClient } from '@/lib/core/config/redis' +import { toError } from '@/lib/core/utils/helpers' import type { ExecutionEvent } from '@/lib/workflows/executor/execution-events' const logger = createLogger('ExecutionEventBuffer') @@ -67,7 +68,7 @@ export async function setExecutionMeta( } catch (error) { logger.warn('Failed to update execution meta', { executionId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -86,7 +87,7 @@ export async function getExecutionMeta(executionId: string): Promise { logger.error('Failed to handle file request from PPTX worker', { fileId: msg.fileId, - error: err instanceof Error ? err.message : String(err), + error: toError(err).message, }) if (proc && !settled) { try { diff --git a/apps/sim/lib/knowledge/connectors/sync-engine.ts b/apps/sim/lib/knowledge/connectors/sync-engine.ts index e2551d5b035..bd9ffaf9f1a 100644 --- a/apps/sim/lib/knowledge/connectors/sync-engine.ts +++ b/apps/sim/lib/knowledge/connectors/sync-engine.ts @@ -9,6 +9,7 @@ import { import { createLogger } from '@sim/logger' import { and, eq, gt, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm' import { decryptApiKey } from '@/lib/api-key/crypto' +import { toError } from '@/lib/core/utils/helpers' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import type { DocumentData } from '@/lib/knowledge/documents/service' @@ -181,7 +182,7 @@ export async function dispatchSync( } else { executeSync(connectorId, { fullSync: options?.fullSync }).catch((error) => { logger.error(`Sync failed for connector ${connectorId}`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, requestId, }) }) @@ -567,7 +568,7 @@ export async function executeSync( logger.warn('Failed to enqueue batch for processing — will retry on next sync', { connectorId, count: batchDocs.length, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -677,7 +678,7 @@ export async function executeSync( logger.warn('Failed to enqueue stuck documents for reprocessing', { connectorId, count: stuckDocs.length, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -744,7 +745,7 @@ export async function executeSync( } catch (cleanupError) { logger.error('Failed to clean up after connector deletion', { connectorId, - error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError), + error: toError(cleanupError).message, }) } @@ -753,7 +754,7 @@ export async function executeSync( return result } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message logger.error('Sync failed', { connectorId, error: errorMessage }) try { @@ -794,7 +795,7 @@ export async function executeSync( } catch (recoveryError) { logger.error('Failed to record sync failure', { connectorId, - error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError), + error: toError(recoveryError).message, }) } @@ -816,7 +817,7 @@ export async function executeSync( } catch (finallyError) { logger.warn('Failed to reset syncing status in finally block', { connectorId, - error: finallyError instanceof Error ? finallyError.message : String(finallyError), + error: toError(finallyError).message, }) } } @@ -1001,7 +1002,7 @@ async function updateDocument( } catch (error) { logger.warn('Failed to delete old storage file', { documentId: existingDocId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/lib/knowledge/documents/document-processor.ts b/apps/sim/lib/knowledge/documents/document-processor.ts index 2d652e9a11a..4878ed0257d 100644 --- a/apps/sim/lib/knowledge/documents/document-processor.ts +++ b/apps/sim/lib/knowledge/documents/document-processor.ts @@ -13,6 +13,7 @@ import { } from '@/lib/chunkers' import type { ChunkingStrategy, StrategyOptions } from '@/lib/chunkers/types' import { env } from '@/lib/core/config/env' +import { toError } from '@/lib/core/utils/helpers' import { parseBuffer, parseFile } from '@/lib/file-parsers' import type { FileParseMetadata } from '@/lib/file-parsers/types' import { resolveParserExtension } from '@/lib/knowledge/documents/parser-extension' @@ -325,7 +326,7 @@ async function handleFileForOCR( logger.warn( `handleFileForOCR: Failed to download external PDF for page count check, proceeding without batching`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, } ) return { httpsUrl: fileUrl, buffer: undefined } @@ -523,7 +524,7 @@ async function parseWithAzureMistralOCR(fileUrl: string, filename: string, mimeT return { content, processingMethod: 'mistral-ocr' as const, cloudUrl: undefined } } catch (error) { logger.error(`Azure Mistral OCR failed for ${filename}:`, { - message: error instanceof Error ? error.message : String(error), + message: toError(error).message, }) logger.info(`Falling back to file parser: ${filename}`) @@ -583,7 +584,7 @@ async function parseWithMistralOCR( return { content, processingMethod: 'mistral-ocr' as const, cloudUrl } } catch (error) { logger.error(`Mistral OCR failed for ${filename}:`, { - message: error instanceof Error ? error.message : String(error), + message: toError(error).message, }) logger.info(`Falling back to file parser: ${filename}`) @@ -695,7 +696,7 @@ async function processChunk( return { index: chunkIndex, content: null } } catch (error) { logger.error(`Chunk ${chunkIndex + 1}/${totalChunks} failed:`, { - message: error instanceof Error ? error.message : String(error), + message: toError(error).message, }) return { index: chunkIndex, content: null } } finally { @@ -705,7 +706,7 @@ async function processChunk( logger.info(`Cleaned up chunk ${chunkIndex + 1} from S3`) } catch (deleteError) { logger.warn(`Failed to clean up chunk ${chunkIndex + 1} from S3:`, { - message: deleteError instanceof Error ? deleteError.message : String(deleteError), + message: toError(deleteError).message, }) } } diff --git a/apps/sim/lib/knowledge/documents/service.ts b/apps/sim/lib/knowledge/documents/service.ts index 92f9318a466..ad160f1836c 100644 --- a/apps/sim/lib/knowledge/documents/service.ts +++ b/apps/sim/lib/knowledge/documents/service.ts @@ -30,6 +30,7 @@ import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing' import type { ChunkingStrategy, StrategyOptions } from '@/lib/chunkers/types' import { env } from '@/lib/core/config/env' import { getCostMultiplier, isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { processDocument } from '@/lib/knowledge/documents/document-processor' import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types' @@ -1820,7 +1821,7 @@ export async function deleteDocumentStorageFiles( } catch (error) { logger.warn(`[${requestId}] Failed to delete document storage file`, { documentId: doc.id, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } }) diff --git a/apps/sim/lib/knowledge/documents/utils.ts b/apps/sim/lib/knowledge/documents/utils.ts index 95708063f82..af3b3561409 100644 --- a/apps/sim/lib/knowledge/documents/utils.ts +++ b/apps/sim/lib/knowledge/documents/utils.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { sleep, toError } from '@/lib/core/utils/helpers' const logger = createLogger('RetryUtils') @@ -53,7 +54,7 @@ export function isRetryableError(error: unknown): boolean { } // Check for network-level errors (DNS, connection, timeout) - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message const lowerMessage = errorMessage.toLowerCase() const networkKeywords = [ @@ -114,7 +115,7 @@ export async function retryWithExponentialBackoff( return result } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)) + lastError = toError(error) logger.warn(`Operation failed on attempt ${attempt + 1}`, { error }) // If this is the last attempt, throw the error @@ -147,7 +148,7 @@ export async function retryWithExponentialBackoff( `Retrying in ${Math.round(actualDelay)}ms (attempt ${attempt + 1}/${maxRetries + 1})${cappedRetryAfter ? ' (Retry-After)' : ''}` ) - await new Promise((resolve) => setTimeout(resolve, actualDelay)) + await sleep(actualDelay) // Exponential backoff (skip if we used Retry-After) if (!cappedRetryAfter) { diff --git a/apps/sim/lib/logs/execution/logging-session.ts b/apps/sim/lib/logs/execution/logging-session.ts index 59a03a32a2c..15911376917 100644 --- a/apps/sim/lib/logs/execution/logging-session.ts +++ b/apps/sim/lib/logs/execution/logging-session.ts @@ -3,6 +3,7 @@ import { workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq, sql } from 'drizzle-orm' import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' +import { toError } from '@/lib/core/utils/helpers' import { executionLogger } from '@/lib/logs/execution/logger' import { calculateCostSummary, @@ -194,7 +195,7 @@ export class LoggingSession { ) } catch (error) { logger.error(`Failed to persist last started block for execution ${this.executionId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -209,7 +210,7 @@ export class LoggingSession { ) } catch (error) { logger.error(`Failed to persist last completed block for execution ${this.executionId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -355,7 +356,7 @@ export class LoggingSession { this.costFlushed = true } catch (error) { logger.error(`Failed to flush accumulated cost for execution ${this.executionId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -384,7 +385,7 @@ export class LoggingSession { } } catch (error) { logger.error(`Failed to load existing cost for execution ${this.executionId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -507,7 +508,7 @@ export class LoggingSession { requestId: this.requestId, workflowId: this.workflowId, executionId: this.executionId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) throw error @@ -626,7 +627,7 @@ export class LoggingSession { requestId: this.requestId, workflowId: this.workflowId, executionId: this.executionId, - error: enhancedError instanceof Error ? enhancedError.message : String(enhancedError), + error: toError(enhancedError).message, stack: enhancedError instanceof Error ? enhancedError.stack : undefined, }) throw enhancedError @@ -713,7 +714,7 @@ export class LoggingSession { requestId: this.requestId, workflowId: this.workflowId, executionId: this.executionId, - error: cancelError instanceof Error ? cancelError.message : String(cancelError), + error: toError(cancelError).message, stack: cancelError instanceof Error ? cancelError.stack : undefined, }) throw cancelError @@ -800,7 +801,7 @@ export class LoggingSession { requestId: this.requestId, workflowId: this.workflowId, executionId: this.executionId, - error: pauseError instanceof Error ? pauseError.message : String(pauseError), + error: toError(pauseError).message, stack: pauseError instanceof Error ? pauseError.stack : undefined, }) throw pauseError @@ -927,7 +928,7 @@ export class LoggingSession { await this.drainPendingProgressWrites() await this.complete(params) } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error) + const errorMsg = toError(error).message logger.warn( `[${this.requestId || 'unknown'}] Complete failed for execution ${this.executionId}, attempting fallback`, { error: errorMsg } @@ -953,7 +954,7 @@ export class LoggingSession { await this.drainPendingProgressWrites() await this.completeWithError(params) } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error) + const errorMsg = toError(error).message logger.warn( `[${this.requestId || 'unknown'}] CompleteWithError failed for execution ${this.executionId}, attempting fallback`, { error: errorMsg } @@ -985,7 +986,7 @@ export class LoggingSession { await this.drainPendingProgressWrites() await this.completeWithCancellation(params) } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error) + const errorMsg = toError(error).message logger.warn( `[${this.requestId || 'unknown'}] CompleteWithCancellation failed for execution ${this.executionId}, attempting fallback`, { error: errorMsg } @@ -1012,7 +1013,7 @@ export class LoggingSession { await this.drainPendingProgressWrites() await this.completeWithPause(params) } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error) + const errorMsg = toError(error).message logger.warn( `[${this.requestId || 'unknown'}] CompleteWithPause failed for execution ${this.executionId}, attempting fallback`, { error: errorMsg } @@ -1066,7 +1067,7 @@ export class LoggingSession { logger.info(`[${requestId || 'unknown'}] Marked execution ${executionId} as failed`) } catch (error) { logger.error(`Failed to mark execution ${executionId} as failed:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } @@ -1147,7 +1148,7 @@ export class LoggingSession { this.completionAttemptFailed = true logger.error( `[${this.requestId || 'unknown'}] Cost-only fallback also failed for execution ${this.executionId}:`, - { error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError) } + { error: toError(fallbackError).message } ) } } diff --git a/apps/sim/lib/mcp/domain-check.ts b/apps/sim/lib/mcp/domain-check.ts index fa09d163e96..7985cf795ff 100644 --- a/apps/sim/lib/mcp/domain-check.ts +++ b/apps/sim/lib/mcp/domain-check.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import * as ipaddr from 'ipaddr.js' import { getAllowedMcpDomainsFromEnv } from '@/lib/core/config/feature-flags' import { isPrivateOrReservedIP } from '@/lib/core/security/input-validation.server' +import { toError } from '@/lib/core/utils/helpers' import { createEnvVarPattern } from '@/executor/utils/reference-validation' const logger = createLogger('McpDomainCheck') @@ -173,7 +174,7 @@ export async function validateMcpServerSsrf(url: string | undefined): Promise>( `[${(authResult as AuthResult).context.requestId}] Error in MCP route handler:`, error ) - return createMcpErrorResponse( - error instanceof Error ? error : new Error('Internal server error'), - 'Internal server error', - 500 - ) + return createMcpErrorResponse(toError(error), 'Internal server error', 500) } } } diff --git a/apps/sim/lib/mcp/service.ts b/apps/sim/lib/mcp/service.ts index 44326c31602..072cee8247d 100644 --- a/apps/sim/lib/mcp/service.ts +++ b/apps/sim/lib/mcp/service.ts @@ -7,6 +7,7 @@ import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import { isTest } from '@/lib/core/config/feature-flags' +import { sleep, toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { McpClient } from '@/lib/mcp/client' import { mcpConnectionManager } from '@/lib/mcp/connection-manager' @@ -211,7 +212,7 @@ class McpService { `[${requestId}] Session error executing tool ${toolCall.name}, retrying (attempt ${attempt + 1}):`, error ) - await new Promise((resolve) => setTimeout(resolve, 100)) + await sleep(100) continue } throw error @@ -225,7 +226,7 @@ class McpService { * Check if an error indicates a session-related issue that might be resolved by retry */ private isSessionError(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error) + const message = toError(error).message const lowerMessage = message.toLowerCase() return ( lowerMessage.includes('session') || @@ -465,7 +466,7 @@ class McpService { `[${requestId}] Session error discovering tools from server ${serverId}, retrying (attempt ${attempt + 1}):`, error ) - await new Promise((resolve) => setTimeout(resolve, 100)) + await sleep(100) continue } throw error diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 70525b7127e..a83105d0e48 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -51,6 +51,7 @@ import { ZoomIcon, } from '@/components/icons' import { env } from '@/lib/core/config/env' +import { toError } from '@/lib/core/utils/helpers' import type { OAuthProviderConfig } from './types' const logger = createLogger('OAuth') @@ -1563,7 +1564,7 @@ export async function refreshOAuthToken( } } catch (error) { logger.error('Error refreshing token:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return null } diff --git a/apps/sim/lib/og/capture-preview.ts b/apps/sim/lib/og/capture-preview.ts index a15b597517b..83818e707ef 100644 --- a/apps/sim/lib/og/capture-preview.ts +++ b/apps/sim/lib/og/capture-preview.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { sleep } from '@/lib/core/utils/helpers' const logger = createLogger('OGCapturePreview') @@ -65,7 +66,7 @@ export async function captureWorkflowPreview( } if (attempt < retries) { - await new Promise((resolve) => setTimeout(resolve, 500 * attempt)) + await sleep(500 * attempt) } } diff --git a/apps/sim/lib/tokenization/calculators.ts b/apps/sim/lib/tokenization/calculators.ts index b0ebc92c5ee..e163a3a4969 100644 --- a/apps/sim/lib/tokenization/calculators.ts +++ b/apps/sim/lib/tokenization/calculators.ts @@ -3,6 +3,7 @@ */ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { createTokenizationError } from '@/lib/tokenization/errors' import { estimateInputTokens, @@ -95,7 +96,7 @@ export function calculateStreamingCost( model, inputLength: inputText?.length || 0, outputLength: outputText?.length || 0, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) if (error instanceof Error && error.name === 'TokenizationError') { @@ -104,7 +105,7 @@ export function calculateStreamingCost( throw createTokenizationError( 'CALCULATION_FAILED', - `Failed to calculate streaming cost: ${error instanceof Error ? error.message : String(error)}`, + `Failed to calculate streaming cost: ${toError(error).message}`, { model, inputLength: inputText?.length || 0, outputLength: outputText?.length || 0 } ) } diff --git a/apps/sim/lib/tokenization/streaming.ts b/apps/sim/lib/tokenization/streaming.ts index b7304b18fd0..547e375389d 100644 --- a/apps/sim/lib/tokenization/streaming.ts +++ b/apps/sim/lib/tokenization/streaming.ts @@ -3,6 +3,7 @@ */ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { calculateStreamingCost } from '@/lib/tokenization/calculators' import { TOKENIZATION_CONFIG } from '@/lib/tokenization/constants' import { @@ -82,7 +83,7 @@ export function processStreamingBlockLog(log: BlockLog, streamedContent: string) } catch (error) { logger.error(`Streaming tokenization failed for block ${log.blockId}`, { blockType: log.blockType, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, contentLength: streamedContent?.length || 0, }) diff --git a/apps/sim/lib/tokenization/utils.ts b/apps/sim/lib/tokenization/utils.ts index 67cf2f8d008..c94bcc0857c 100644 --- a/apps/sim/lib/tokenization/utils.ts +++ b/apps/sim/lib/tokenization/utils.ts @@ -3,6 +3,7 @@ */ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { LLM_BLOCK_TYPES, MAX_PREVIEW_LENGTH, @@ -38,7 +39,7 @@ export function getProviderForTokenization(model: string): string { } catch (error) { logger.warn(`Failed to get provider for model ${model}, using default`, { model, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return TOKENIZATION_CONFIG.defaults.provider } @@ -86,7 +87,7 @@ export function extractTextContent(input: unknown): string { } catch (error) { logger.warn('Failed to stringify input object', { inputType: typeof input, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return '' } diff --git a/apps/sim/lib/webhooks/pending-verification.ts b/apps/sim/lib/webhooks/pending-verification.ts index 02c50204f92..db38b6f6a11 100644 --- a/apps/sim/lib/webhooks/pending-verification.ts +++ b/apps/sim/lib/webhooks/pending-verification.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getRedisClient } from '@/lib/core/config/redis' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('WebhookPendingVerification') @@ -159,7 +160,7 @@ export async function getPendingWebhookVerification( } catch (error) { logger.warn('Failed to parse pending webhook verification entry', { path, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) await redis.del(getRedisKey(path)) return null diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index 6cad489554d..e8b28b753eb 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -8,6 +8,7 @@ import { tryAdmit } from '@/lib/core/admission/gate' import { getInlineJobQueue, getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs' import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types' import { isProd } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' import { preprocessExecution } from '@/lib/execution/preprocessing' @@ -83,7 +84,7 @@ export async function parseWebhookBody( } } catch (bodyError) { logger.error(`[${requestId}] Failed to read request body`, { - error: bodyError instanceof Error ? bodyError.message : String(bodyError), + error: toError(bodyError).message, }) return new NextResponse('Failed to read request body', { status: 400 }) } @@ -106,7 +107,7 @@ export async function parseWebhookBody( } } catch (parseError) { logger.error(`[${requestId}] Failed to parse webhook body`, { - error: parseError instanceof Error ? parseError.message : String(parseError), + error: toError(parseError).message, contentType: request.headers.get('content-type'), bodyPreview: `${rawBody?.slice(0, 100)}...`, }) @@ -597,7 +598,7 @@ export async function queueWebhookExecution( const output = await executeWebhookJob(payload) await jobQueue.completeJob(jobId, output) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message logger.error(`[${options.requestId}] Webhook execution failed`, { jobId, error: errorMessage, @@ -784,7 +785,7 @@ export async function processPolledWebhookEvent( const output = await executeWebhookJob(payload) await jobQueue.completeJob(jobId, output) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message logger.error(`[${requestId}] Webhook execution failed`, { jobId, error: errorMessage, diff --git a/apps/sim/lib/webhooks/provider-subscriptions.ts b/apps/sim/lib/webhooks/provider-subscriptions.ts index cbf6a05f184..3c6060ce0c6 100644 --- a/apps/sim/lib/webhooks/provider-subscriptions.ts +++ b/apps/sim/lib/webhooks/provider-subscriptions.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' +import { toError } from '@/lib/core/utils/helpers' import { getProviderHandler } from '@/lib/webhooks/providers' const logger = createLogger('WebhookProviderSubscriptions') @@ -138,7 +139,7 @@ export async function cleanupExternalWebhook( logger.warn(`[${requestId}] Error cleaning up external webhook (non-fatal)`, { provider, webhookId: webhook.id, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/lib/webhooks/providers/attio.ts b/apps/sim/lib/webhooks/providers/attio.ts index 84c6b740780..6c23b6939f7 100644 --- a/apps/sim/lib/webhooks/providers/attio.ts +++ b/apps/sim/lib/webhooks/providers/attio.ts @@ -2,6 +2,7 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { safeCompare } from '@/lib/core/security/encryption' +import { toError } from '@/lib/core/utils/helpers' import { getBaseUrl } from '@/lib/core/utils/urls' import { getCredentialOwner, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { @@ -237,7 +238,7 @@ export const attioHandler: WebhookProviderHandler = { return { providerConfigUpdates: { externalId: webhookId, webhookSecret: secret || '' } } } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) + const message = toError(error).message logger.error( `[${requestId}] Exception during Attio webhook creation for webhook ${webhookRecord.id}.`, { message } diff --git a/apps/sim/lib/webhooks/providers/gong.ts b/apps/sim/lib/webhooks/providers/gong.ts index 6a45d60ff3f..6a836892072 100644 --- a/apps/sim/lib/webhooks/providers/gong.ts +++ b/apps/sim/lib/webhooks/providers/gong.ts @@ -2,6 +2,7 @@ import { createHash } from 'node:crypto' import { createLogger } from '@sim/logger' import * as jose from 'jose' import { NextResponse } from 'next/server' +import { toError } from '@/lib/core/utils/helpers' import type { AuthContext, FormatInputContext, @@ -84,7 +85,7 @@ export async function verifyGongJwtAuth(ctx: AuthContext): Promise {}) diff --git a/apps/sim/lib/workflows/executor/pause-persistence.ts b/apps/sim/lib/workflows/executor/pause-persistence.ts index c522fb5eeae..019f2084139 100644 --- a/apps/sim/lib/workflows/executor/pause-persistence.ts +++ b/apps/sim/lib/workflows/executor/pause-persistence.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { LoggingSession } from '@/lib/logs/execution/logging-session' import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' import type { ExecutionResult } from '@/executor/types' @@ -44,10 +45,10 @@ export async function handlePostExecutionPauseState({ } catch (pauseError) { logger.error('Failed to persist pause result', { executionId, - error: pauseError instanceof Error ? pauseError.message : String(pauseError), + error: toError(pauseError).message, }) await loggingSession.markAsFailed( - `Failed to persist pause state: ${pauseError instanceof Error ? pauseError.message : String(pauseError)}` + `Failed to persist pause state: ${toError(pauseError).message}` ) } } @@ -57,7 +58,7 @@ export async function handlePostExecutionPauseState({ } catch (resumeError) { logger.error('Failed to process queued resumes', { executionId, - error: resumeError instanceof Error ? resumeError.message : String(resumeError), + error: toError(resumeError).message, }) } } diff --git a/apps/sim/lib/workflows/executor/queued-workflow-execution.ts b/apps/sim/lib/workflows/executor/queued-workflow-execution.ts index 75831b27b95..c37fefef20d 100644 --- a/apps/sim/lib/workflows/executor/queued-workflow-execution.ts +++ b/apps/sim/lib/workflows/executor/queued-workflow-execution.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { createTimeoutAbortController, getTimeoutErrorMessage } from '@/lib/core/execution-limits' +import { toError } from '@/lib/core/utils/helpers' import { createExecutionEventWriter, setExecutionMeta } from '@/lib/execution/event-buffer' import { LoggingSession } from '@/lib/logs/execution/logging-session' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' @@ -273,7 +274,7 @@ export async function executeQueuedWorkflowJob( logger.error('Queued workflow execution failed', { workflowId, executionId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) if (!wasExecutionFinalizedByCore(error, executionId)) { @@ -281,7 +282,7 @@ export async function executeQueuedWorkflowJob( const { traceSpans } = executionResult ? buildTraceSpans(executionResult) : { traceSpans: [] } await loggingSession.safeCompleteWithError({ error: { - message: error instanceof Error ? error.message : String(error), + message: toError(error).message, stackTrace: error instanceof Error ? error.stack : undefined, }, traceSpans, @@ -295,7 +296,7 @@ export async function executeQueuedWorkflowJob( executionId, workflowId, data: { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, duration: 0, }, }) @@ -309,7 +310,7 @@ export async function executeQueuedWorkflowJob( { success: false, output: executionResult?.output ?? {}, - error: executionResult?.error || (error instanceof Error ? error.message : String(error)), + error: executionResult?.error || toError(error).message, logs: executionResult?.logs, metadata: executionResult?.metadata ? { @@ -332,7 +333,7 @@ export async function executeQueuedWorkflowJob( await cleanupExecutionBase64Cache(executionId).catch((error) => { logger.error('Failed to cleanup queued workflow base64 cache', { executionId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) }) } diff --git a/apps/sim/lib/workflows/persistence/custom-tools-persistence.ts b/apps/sim/lib/workflows/persistence/custom-tools-persistence.ts index 90a670095c1..c9f64606f43 100644 --- a/apps/sim/lib/workflows/persistence/custom-tools-persistence.ts +++ b/apps/sim/lib/workflows/persistence/custom-tools-persistence.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations' const logger = createLogger('CustomToolsPersistence') @@ -159,7 +160,7 @@ export async function persistCustomToolsToDatabase( saved = validTools.length logger.info(`Persisted ${saved} custom tool(s)`, { workspaceId }) } catch (error) { - const errorMsg = `Failed to persist custom tools: ${error instanceof Error ? error.message : String(error)}` + const errorMsg = `Failed to persist custom tools: ${toError(error).message}` logger.error(errorMsg, { error }) errors.push(errorMsg) } diff --git a/apps/sim/lib/workflows/sanitization/validation.ts b/apps/sim/lib/workflows/sanitization/validation.ts index 4c25d199819..58defea1269 100644 --- a/apps/sim/lib/workflows/sanitization/validation.ts +++ b/apps/sim/lib/workflows/sanitization/validation.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { getBlock } from '@/blocks/registry' import { isCustomTool, isMcpTool } from '@/executor/constants' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' @@ -161,7 +162,7 @@ export function sanitizeAgentToolsInBlocks(blocks: Record): // Reassign in case caller uses object identity sanitizedBlocks[blockId] = { ...block, subBlocks: { ...subBlocks, tools: toolsSubBlock } } } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err) + const message = toError(err).message warnings.push(`Block ${block?.name || blockId}: tools sanitation failed: ${message}`) } } @@ -308,7 +309,7 @@ export function validateWorkflowState( } } catch (err) { logger.error('Workflow validation failed with exception', err) - errors.push(`Validation failed: ${err instanceof Error ? err.message : String(err)}`) + errors.push(`Validation failed: ${toError(err).message}`) return { valid: false, errors, warnings } } } diff --git a/apps/sim/lib/workflows/schedules/utils.ts b/apps/sim/lib/workflows/schedules/utils.ts index bb1f9549e40..51b42ae04aa 100644 --- a/apps/sim/lib/workflows/schedules/utils.ts +++ b/apps/sim/lib/workflows/schedules/utils.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { Cron } from 'croner' import cronstrue from 'cronstrue' import { formatDateTime } from '@/lib/core/utils/formatting' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('ScheduleUtils') @@ -256,7 +257,7 @@ export function createDateWithTimezone( } catch (fallbackError) { logger.error('Error during fallback date creation:', fallbackError) throw new Error( - `Failed to create date with timezone (${timezone}): ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}` + `Failed to create date with timezone (${timezone}): ${toError(fallbackError).message}` ) } } @@ -428,7 +429,7 @@ export function calculateNextRunTime( } catch (error) { logger.error('Error calculating next run with Croner:', error) throw new Error( - `Failed to calculate next run time for schedule type ${scheduleType}: ${error instanceof Error ? error.message : String(error)}` + `Failed to calculate next run time for schedule type ${scheduleType}: ${toError(error).message}` ) } } @@ -480,7 +481,7 @@ export const parseCronToHumanReadable = (cronExpression: string, timezone?: stri } catch (error) { logger.warn('Failed to parse cron expression with cronstrue:', { cronExpression, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return `Schedule: ${cronExpression}${timezone && timezone !== 'UTC' ? ` (${getTimezoneAbbreviation(timezone)})` : ''}` } diff --git a/apps/sim/lib/workflows/triggers/trigger-utils.ts b/apps/sim/lib/workflows/triggers/trigger-utils.ts index 699cbc51756..e2b1d4e5aa4 100644 --- a/apps/sim/lib/workflows/triggers/trigger-utils.ts +++ b/apps/sim/lib/workflows/triggers/trigger-utils.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' import { type StartBlockCandidate, @@ -451,7 +452,7 @@ export function extractTriggerMockPayload< logger.error('Failed to generate mock payload from trigger outputs', { triggerId, blockId: trigger.blockId, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return {} } diff --git a/apps/sim/providers/anthropic/core.ts b/apps/sim/providers/anthropic/core.ts index 457b63a9613..756cd8f248a 100644 --- a/apps/sim/providers/anthropic/core.ts +++ b/apps/sim/providers/anthropic/core.ts @@ -2,6 +2,7 @@ import type Anthropic from '@anthropic-ai/sdk' import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema' import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages/messages' import type { Logger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { @@ -871,7 +872,7 @@ export async function executeAnthropicProviderRequest( duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, @@ -1328,7 +1329,7 @@ export async function executeAnthropicProviderRequest( duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index 72ee513e80e..a1ef02578fc 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -10,6 +10,7 @@ import type { } from 'openai/resources/chat/completions' import type { ReasoningEffort } from 'openai/resources/shared' import { env } from '@/lib/core/config/env' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { @@ -593,7 +594,7 @@ async function executeChatCompletionsRequest( duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts index d3223cfebd0..60d5dd36a13 100644 --- a/apps/sim/providers/bedrock/index.ts +++ b/apps/sim/providers/bedrock/index.ts @@ -13,6 +13,7 @@ import { type ToolUseBlock, } from '@aws-sdk/client-bedrock-runtime' import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { @@ -928,7 +929,7 @@ export const bedrockProvider: ProviderConfig = { duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/cerebras/index.ts b/apps/sim/providers/cerebras/index.ts index 9ef64836030..f1f111c267e 100644 --- a/apps/sim/providers/cerebras/index.ts +++ b/apps/sim/providers/cerebras/index.ts @@ -1,5 +1,6 @@ import { Cerebras } from '@cerebras/cerebras_cloud_sdk' import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import type { CerebrasResponse } from '@/providers/cerebras/types' @@ -267,7 +268,7 @@ export const cerebrasProvider: ProviderConfig = { } catch (error) { const toolCallEndTime = Date.now() logger.error('Error processing tool call (Cerebras):', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, toolName, }) @@ -555,7 +556,7 @@ export const cerebrasProvider: ProviderConfig = { duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/deepseek/index.ts b/apps/sim/providers/deepseek/index.ts index 692fb270591..5b60ec1e78b 100644 --- a/apps/sim/providers/deepseek/index.ts +++ b/apps/sim/providers/deepseek/index.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { createReadableStreamFromDeepseekStream } from '@/providers/deepseek/utils' @@ -555,7 +556,7 @@ export const deepseekProvider: ProviderConfig = { duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/fireworks/index.ts b/apps/sim/providers/fireworks/index.ts index b5dd9d41964..9fd4ff1641b 100644 --- a/apps/sim/providers/fireworks/index.ts +++ b/apps/sim/providers/fireworks/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { @@ -310,7 +311,7 @@ export const fireworksProvider: ProviderConfig = { } catch (error) { const toolCallEndTime = Date.now() logger.error('Error processing tool call (Fireworks):', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, toolName, }) @@ -600,7 +601,7 @@ export const fireworksProvider: ProviderConfig = { const totalDuration = providerEndTime - providerStartTime const errorDetails: Record = { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, duration: totalDuration, } if (error && typeof error === 'object') { @@ -613,7 +614,7 @@ export const fireworksProvider: ProviderConfig = { } logger.error('Error in Fireworks request:', errorDetails) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/gemini/core.ts b/apps/sim/providers/gemini/core.ts index cba0a8a6fd2..851b70fba25 100644 --- a/apps/sim/providers/gemini/core.ts +++ b/apps/sim/providers/gemini/core.ts @@ -12,6 +12,7 @@ import { type ToolConfig, } from '@google/genai' import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { @@ -145,7 +146,7 @@ async function executeToolCallsBatch( } catch (error) { const toolCallEndTime = Date.now() logger.error('Error processing function call:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, functionName: toolName, }) return { @@ -621,7 +622,7 @@ function createDeepResearchStream( controller.close() } catch (error) { streamLogger.error('Error reading deep research stream', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) controller.error(error) } @@ -861,11 +862,11 @@ export async function executeDeepResearchRequest( const duration = providerEndTime - providerStartTime logger.error('Error in deep research request:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) - const enhancedError = error instanceof Error ? error : new Error(String(error)) + const enhancedError = toError(error) Object.assign(enhancedError, { timing: { startTime: providerStartTimeISO, @@ -1241,11 +1242,11 @@ export async function executeGeminiRequest( const duration = providerEndTime - providerStartTime logger.error('Error in Gemini request:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) - const enhancedError = error instanceof Error ? error : new Error(String(error)) + const enhancedError = toError(error) Object.assign(enhancedError, { timing: { startTime: providerStartTimeISO, diff --git a/apps/sim/providers/google/utils.ts b/apps/sim/providers/google/utils.ts index 0a23b50dacb..48f1d43ecc1 100644 --- a/apps/sim/providers/google/utils.ts +++ b/apps/sim/providers/google/utils.ts @@ -13,6 +13,7 @@ import { Type, } from '@google/genai' import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { ProviderRequest } from '@/providers/types' import { trackForcedToolUsage } from '@/providers/utils' @@ -301,7 +302,7 @@ export function createReadableStreamFromGeminiStream( controller.close() } catch (error) { logger.error('Error reading Google Gemini stream', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) controller.error(error) } diff --git a/apps/sim/providers/groq/index.ts b/apps/sim/providers/groq/index.ts index 8e1ecbabf94..013386f6e4e 100644 --- a/apps/sim/providers/groq/index.ts +++ b/apps/sim/providers/groq/index.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { Groq } from 'groq-sdk' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { createReadableStreamFromGroqStream } from '@/providers/groq/utils' @@ -513,7 +514,7 @@ export const groqProvider: ProviderConfig = { duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/index.ts b/apps/sim/providers/index.ts index ea617e33789..b667b05e722 100644 --- a/apps/sim/providers/index.ts +++ b/apps/sim/providers/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { getApiKeyWithBYOK } from '@/lib/api-key/byok' import { getCostMultiplier } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { getProviderExecutor } from '@/providers/registry' import type { ProviderId, ProviderRequest, ProviderResponse } from '@/providers/types' @@ -122,7 +123,7 @@ export async function executeProviderRequest( logger.error('Failed to resolve API key:', { provider: providerId, model: request.model, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) throw error } diff --git a/apps/sim/providers/mistral/index.ts b/apps/sim/providers/mistral/index.ts index a332ae7b400..dcfc103ef44 100644 --- a/apps/sim/providers/mistral/index.ts +++ b/apps/sim/providers/mistral/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { createReadableStreamFromMistralStream } from '@/providers/mistral/utils' @@ -567,7 +568,7 @@ export const mistralProvider: ProviderConfig = { duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/ollama/index.ts b/apps/sim/providers/ollama/index.ts index c09bf88977b..dcaa0233b1e 100644 --- a/apps/sim/providers/ollama/index.ts +++ b/apps/sim/providers/ollama/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' +import { toError } from '@/lib/core/utils/helpers' import { getOllamaUrl } from '@/lib/core/utils/urls' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' @@ -570,7 +571,7 @@ export const ollamaProvider: ProviderConfig = { duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/openai/core.ts b/apps/sim/providers/openai/core.ts index 312ac025ba9..53e429351b5 100644 --- a/apps/sim/providers/openai/core.ts +++ b/apps/sim/providers/openai/core.ts @@ -1,5 +1,6 @@ import type { Logger } from '@sim/logger' import type OpenAI from 'openai' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types' @@ -813,7 +814,7 @@ export async function executeResponsesProviderRequest( duration: totalDuration, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/openrouter/index.ts b/apps/sim/providers/openrouter/index.ts index 7b01fa5784a..2c57366aac6 100644 --- a/apps/sim/providers/openrouter/index.ts +++ b/apps/sim/providers/openrouter/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' @@ -311,7 +312,7 @@ export const openRouterProvider: ProviderConfig = { } catch (error) { const toolCallEndTime = Date.now() logger.error('Error processing tool call (OpenRouter):', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, toolName, }) @@ -601,7 +602,7 @@ export const openRouterProvider: ProviderConfig = { const totalDuration = providerEndTime - providerStartTime const errorDetails: Record = { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, duration: totalDuration, } if (error && typeof error === 'object') { @@ -614,7 +615,7 @@ export const openRouterProvider: ProviderConfig = { } logger.error('Error in OpenRouter request:', errorDetails) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/providers/openrouter/utils.ts b/apps/sim/providers/openrouter/utils.ts index 6591dfdc3d3..a914b036e33 100644 --- a/apps/sim/providers/openrouter/utils.ts +++ b/apps/sim/providers/openrouter/utils.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' +import { toError } from '@/lib/core/utils/helpers' import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' const logger = createLogger('OpenRouterUtils') @@ -57,7 +58,7 @@ async function fetchModelCapabilities(): Promise> return capabilities } catch (error) { logger.error('Error fetching OpenRouter model capabilities', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return new Map() } diff --git a/apps/sim/providers/vllm/index.ts b/apps/sim/providers/vllm/index.ts index e4f0a4c93e8..ae053017176 100644 --- a/apps/sim/providers/vllm/index.ts +++ b/apps/sim/providers/vllm/index.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' import { env } from '@/lib/core/config/env' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' @@ -633,7 +634,7 @@ export const vllmProvider: ProviderConfig = { const providerEndTimeISO = new Date(providerEndTime).toISOString() const totalDuration = providerEndTime - providerStartTime - let errorMessage = error instanceof Error ? error.message : String(error) + let errorMessage = toError(error).message let errorType: string | undefined let errorCode: number | undefined diff --git a/apps/sim/providers/xai/index.ts b/apps/sim/providers/xai/index.ts index cfd2f3b784e..cd7d8b28f8f 100644 --- a/apps/sim/providers/xai/index.ts +++ b/apps/sim/providers/xai/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' +import { toError } from '@/lib/core/utils/helpers' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' @@ -284,7 +285,7 @@ export const xAIProvider: ProviderConfig = { } catch (error) { const toolCallEndTime = Date.now() logger.error('XAI Provider - Error processing tool call:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, toolCall: toolCall.function.name, }) @@ -462,7 +463,7 @@ export const xAIProvider: ProviderConfig = { } } catch (error) { logger.error('XAI Provider - Error in tool processing loop:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, iterationCount, }) } @@ -599,13 +600,13 @@ export const xAIProvider: ProviderConfig = { const totalDuration = providerEndTime - providerStartTime logger.error('XAI Provider - Request failed:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, duration: totalDuration, hasTools: !!tools?.length, hasResponseFormat: !!request.responseFormat, }) - throw new ProviderError(error instanceof Error ? error.message : String(error), { + throw new ProviderError(toError(error).message, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 83fd292bb68..d570f982066 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import type { Edge } from 'reactflow' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility' import { @@ -544,7 +545,7 @@ export class Serializer { : blockConfig.tools.access[0] } catch (error) { logger.warn('Tool selection failed during serialization, using default:', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) return blockConfig.tools.access[0] } diff --git a/apps/sim/socket/middleware/auth.ts b/apps/sim/socket/middleware/auth.ts index b334902d8cd..05ac073b882 100644 --- a/apps/sim/socket/middleware/auth.ts +++ b/apps/sim/socket/middleware/auth.ts @@ -3,6 +3,7 @@ import type { Socket } from 'socket.io' import { auth } from '@/lib/auth' import { ANONYMOUS_USER, ANONYMOUS_USER_ID } from '@/lib/auth/constants' import { isAuthDisabled } from '@/lib/core/config/feature-flags' +import { toError } from '@/lib/core/utils/helpers' const logger = createLogger('SocketAuth') @@ -75,7 +76,7 @@ export async function authenticateSocket(socket: AuthenticatedSocket, next: (err next() } catch (tokenError) { - const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError) + const errorMessage = toError(tokenError).message const errorStack = tokenError instanceof Error ? tokenError.stack : undefined logger.warn(`Token validation failed for socket ${socket.id}:`, { diff --git a/apps/sim/stores/workflow-diff/store.ts b/apps/sim/stores/workflow-diff/store.ts index a901e68d0f2..eac3b271de1 100644 --- a/apps/sim/stores/workflow-diff/store.ts +++ b/apps/sim/stores/workflow-diff/store.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { COPILOT_STATS_API_PATH } from '@/lib/copilot/constants' +import { toError } from '@/lib/core/utils/helpers' import { stripWorkflowDiffMarkers, WorkflowDiffEngine } from '@/lib/workflows/diff' import { enqueueReplaceWorkflowState } from '@/lib/workflows/operations/socket-operations' import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' @@ -334,7 +335,7 @@ export const useWorkflowDiffStore = create { logger.warn('Failed to send diff-accepted stats', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, messageId: triggerMessageId, }) }) @@ -427,7 +428,7 @@ export const useWorkflowDiffStore = create { logger.warn('Failed to send diff-rejected stats', { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, messageId: _triggerMessageId, }) }) diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 8044a16bf38..6b0fae892a0 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { Edge } from 'reactflow' import { create } from 'zustand' import { devtools } from 'zustand/middleware' +import { toError } from '@/lib/core/utils/helpers' import { generateId } from '@/lib/core/utils/uuid' import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants' import { @@ -73,7 +74,7 @@ function resolveInitialSubblockValue(config: SubBlockConfig): unknown { } catch (error) { logger.warn('Failed to resolve dynamic sub-block default value', { subBlockId: config.id, - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) } } diff --git a/apps/sim/tools/agiloft/attachment_info.ts b/apps/sim/tools/agiloft/attachment_info.ts index 26b9969b4ff..38471b74e3b 100644 --- a/apps/sim/tools/agiloft/attachment_info.ts +++ b/apps/sim/tools/agiloft/attachment_info.ts @@ -82,18 +82,18 @@ export const agiloftAttachmentInfoTool: ToolConfig< } } - const data = await response.json() - const result = data.result ?? data + const data = (await response.json()) as Record + const result = (data.result ?? data) as Record const attachments: Array<{ position: number; name: string; size: number }> = [] if (Array.isArray(result)) { for (let i = 0; i < result.length; i++) { - const item = result[i] + const item = result[i] as Record attachments.push({ - position: item.position ?? i, - name: item.name ?? item.filename ?? '', - size: item.size ?? 0, + position: (item.position as number) ?? i, + name: (item.name as string) ?? (item.filename as string) ?? '', + size: (item.size as number) ?? 0, }) } } diff --git a/apps/sim/tools/agiloft/create_record.ts b/apps/sim/tools/agiloft/create_record.ts index 01304733706..d89943f9750 100644 --- a/apps/sim/tools/agiloft/create_record.ts +++ b/apps/sim/tools/agiloft/create_record.ts @@ -85,8 +85,8 @@ export const agiloftCreateRecordTool: ToolConfig + const result = (data.result ?? data) as Record const id = result.id ?? result.ID ?? data.id ?? data.ID ?? null return { diff --git a/apps/sim/tools/agiloft/lock_record.ts b/apps/sim/tools/agiloft/lock_record.ts index 324fa3916c3..d79777b1962 100644 --- a/apps/sim/tools/agiloft/lock_record.ts +++ b/apps/sim/tools/agiloft/lock_record.ts @@ -81,17 +81,21 @@ export const agiloftLockRecordTool: ToolConfig + const result = (data.result ?? data) as Record return { success: data.success !== false, output: { id: String(result.id ?? params.recordId?.trim() ?? ''), - lockStatus: result.lock_status ?? result.lockStatus ?? 'UNKNOWN', - lockedBy: result.locked_by ?? result.lockedBy ?? null, + lockStatus: + (result.lock_status as string) ?? (result.lockStatus as string) ?? 'UNKNOWN', + lockedBy: + (result.locked_by as string | null) ?? (result.lockedBy as string | null) ?? null, lockExpiresInMinutes: - result.lock_expires_in_minutes ?? result.lockExpiresInMinutes ?? null, + (result.lock_expires_in_minutes as number | null) ?? + (result.lockExpiresInMinutes as number | null) ?? + null, }, } } diff --git a/apps/sim/tools/agiloft/read_record.ts b/apps/sim/tools/agiloft/read_record.ts index a01a474c0cb..c9760a70089 100644 --- a/apps/sim/tools/agiloft/read_record.ts +++ b/apps/sim/tools/agiloft/read_record.ts @@ -76,8 +76,8 @@ export const agiloftReadRecordTool: ToolConfig + const result = (data.result ?? data) as Record const id = result.id ?? result.ID ?? data.id ?? data.ID ?? null return { diff --git a/apps/sim/tools/agiloft/saved_search.ts b/apps/sim/tools/agiloft/saved_search.ts index 79dacc03573..8b28cbd140b 100644 --- a/apps/sim/tools/agiloft/saved_search.ts +++ b/apps/sim/tools/agiloft/saved_search.ts @@ -67,8 +67,8 @@ export const agiloftSavedSearchTool: ToolConfig< } } - const data = await response.json() - const result = data.result ?? data + const data = (await response.json()) as Record + const result = (data.result ?? data) as Record const searches: Array<{ name: string @@ -78,12 +78,12 @@ export const agiloftSavedSearchTool: ToolConfig< }> = [] if (Array.isArray(result)) { - for (const item of result) { + for (const item of result as Record[]) { searches.push({ - name: item.name ?? '', - label: item.label ?? item.name ?? '', - id: item.id ?? item.ID ?? '', - description: item.description ?? null, + name: (item.name as string) ?? '', + label: (item.label as string) ?? (item.name as string) ?? '', + id: (item.id as string | number) ?? (item.ID as string | number) ?? '', + description: (item.description as string | null) ?? null, }) } } diff --git a/apps/sim/tools/agiloft/search_records.ts b/apps/sim/tools/agiloft/search_records.ts index 32a41e3b239..422140a81aa 100644 --- a/apps/sim/tools/agiloft/search_records.ts +++ b/apps/sim/tools/agiloft/search_records.ts @@ -92,23 +92,23 @@ export const agiloftSearchRecordsTool: ToolConfig< } } - const data = await response.json() + const data = (await response.json()) as Record const records: Record[] = [] if (data.result && Array.isArray(data.result)) { - for (const item of data.result) { + for (const item of data.result as Record[]) { records.push(item) } } else if (Array.isArray(data)) { - for (const item of data) { + for (const item of data as Record[]) { records.push(item) } } else if (data.results && Array.isArray(data.results)) { - for (const item of data.results) { + for (const item of data.results as Record[]) { records.push(item) } } else if (data.records && Array.isArray(data.records)) { - for (const item of data.records) { + for (const item of data.records as Record[]) { records.push(item) } } else if (typeof data.EWREST_length === 'number') { @@ -128,7 +128,11 @@ export const agiloftSearchRecordsTool: ToolConfig< } const totalCount = - data.totalCount ?? data.total ?? data.count ?? data.EWREST_length ?? records.length + (data.totalCount as number) ?? + (data.total as number) ?? + (data.count as number) ?? + (data.EWREST_length as number) ?? + records.length const page = params.page ? Number(params.page) : 0 const limit = params.limit ? Number(params.limit) : 25 diff --git a/apps/sim/tools/agiloft/select_records.ts b/apps/sim/tools/agiloft/select_records.ts index f580dd13ae9..521ea497fbd 100644 --- a/apps/sim/tools/agiloft/select_records.ts +++ b/apps/sim/tools/agiloft/select_records.ts @@ -74,12 +74,12 @@ export const agiloftSelectRecordsTool: ToolConfig< } } - const data = await response.json() - const result = data.result ?? data + const data = (await response.json()) as Record + const result = (data.result ?? data) as Record const recordIds: string[] = [] if (Array.isArray(result)) { - for (const item of result) { + for (const item of result as Record[]) { const id = item.id ?? item.ID ?? item recordIds.push(String(id)) } diff --git a/apps/sim/tools/agiloft/update_record.ts b/apps/sim/tools/agiloft/update_record.ts index e370e61b684..0c3f8a2d096 100644 --- a/apps/sim/tools/agiloft/update_record.ts +++ b/apps/sim/tools/agiloft/update_record.ts @@ -91,8 +91,8 @@ export const agiloftUpdateRecordTool: ToolConfig + const result = (data.result ?? data) as Record const id = result.id ?? result.ID ?? data.id ?? data.ID ?? null return { diff --git a/apps/sim/tools/agiloft/utils.ts b/apps/sim/tools/agiloft/utils.ts index 252dcb4a819..65c9deff49e 100644 --- a/apps/sim/tools/agiloft/utils.ts +++ b/apps/sim/tools/agiloft/utils.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { validateExternalUrl } from '@/lib/core/security/input-validation' +import type { SecureFetchResponse } from '@/lib/core/security/input-validation.server' import type { AgiloftAttachmentInfoParams, AgiloftBaseParams, @@ -16,6 +16,18 @@ import type { HttpMethod, ToolResponse } from '@/tools/types' const logger = createLogger('AgiloftAuth') +/** + * Lazily imports server-only security functions to avoid pulling `dns/promises` + * into client bundles (this file is reachable from tools/registry.ts). + */ +async function getServerSecurity() { + const mod = await import('@/lib/core/security/input-validation.server') + return { + secureFetchWithPinnedIP: mod.secureFetchWithPinnedIP, + validateUrlWithDNS: mod.validateUrlWithDNS, + } +} + interface AgiloftRequestConfig { url: string method: HttpMethod @@ -23,30 +35,40 @@ interface AgiloftRequestConfig { body?: BodyInit } +/** + * Validates the instance URL via DNS resolution and returns the resolved IP + * for use with pinned fetches to prevent SSRF via DNS rebinding. + */ +async function validateInstanceUrl(instanceUrl: string): Promise { + const { validateUrlWithDNS } = await getServerSecurity() + const validation = await validateUrlWithDNS(instanceUrl, 'instanceUrl') + if (!validation.isValid) { + throw new Error(`Invalid Agiloft instance URL: ${validation.error}`) + } + return validation.resolvedIP! +} + /** * Exchanges login/password for a short-lived Bearer token via EWLogin. + * Uses DNS-pinned fetch to prevent SSRF via DNS rebinding. */ -async function agiloftLogin(params: AgiloftBaseParams): Promise { +async function agiloftLogin(params: AgiloftBaseParams, resolvedIP: string): Promise { const base = params.instanceUrl.replace(/\/$/, '') - const urlValidation = validateExternalUrl(params.instanceUrl, 'instanceUrl') - if (!urlValidation.isValid) { - throw new Error(`Invalid Agiloft instance URL: ${urlValidation.error}`) - } - const kb = encodeURIComponent(params.knowledgeBase) const login = encodeURIComponent(params.login) const password = encodeURIComponent(params.password) const url = `${base}/ewws/EWLogin?$KB=${kb}&$login=${login}&$password=${password}` - const response = await fetch(url, { method: 'POST' }) + const { secureFetchWithPinnedIP } = await getServerSecurity() + const response = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'POST' }) if (!response.ok) { const errorText = await response.text() throw new Error(`Agiloft login failed: ${response.status} - ${errorText}`) } - const data = await response.json() + const data = (await response.json()) as { access_token?: string } const token = data.access_token if (!token) { @@ -58,16 +80,19 @@ async function agiloftLogin(params: AgiloftBaseParams): Promise { /** * Cleans up the server session. Best-effort — failures are logged but not thrown. + * Uses DNS-pinned fetch to prevent SSRF via DNS rebinding. */ async function agiloftLogout( instanceUrl: string, knowledgeBase: string, - token: string + token: string, + resolvedIP: string ): Promise { try { const base = instanceUrl.replace(/\/$/, '') const kb = encodeURIComponent(knowledgeBase) - await fetch(`${base}/ewws/EWLogout?$KB=${kb}`, { + const { secureFetchWithPinnedIP } = await getServerSecurity() + await secureFetchWithPinnedIP(`${base}/ewws/EWLogout?$KB=${kb}`, resolvedIP, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }) @@ -78,42 +103,43 @@ async function agiloftLogout( /** * Shared wrapper that handles the full auth lifecycle: - * 1. Login to get Bearer token - * 2. Execute the request with the token - * 3. Logout to clean up the session + * 1. Validate instance URL via DNS resolution + * 2. Login to get Bearer token (using pinned IP) + * 3. Execute the request with the token (using pinned IP) + * 4. Logout to clean up the session (using pinned IP) * - * The `buildRequest` callback receives the token and base URL, and returns - * the request config. The `transformResponse` callback converts the raw - * Response into the tool's output format. + * All HTTP requests use the resolved IP to prevent SSRF via DNS rebinding. */ export async function executeAgiloftRequest( params: AgiloftBaseParams, buildRequest: (base: string) => AgiloftRequestConfig, - transformResponse: (response: Response) => Promise + transformResponse: (response: SecureFetchResponse) => Promise ): Promise { - const token = await agiloftLogin(params) + const resolvedIP = await validateInstanceUrl(params.instanceUrl) + const token = await agiloftLogin(params, resolvedIP) const base = params.instanceUrl.replace(/\/$/, '') try { const req = buildRequest(base) - const response = await fetch(req.url, { + const { secureFetchWithPinnedIP } = await getServerSecurity() + const response = await secureFetchWithPinnedIP(req.url, resolvedIP, { method: req.method, headers: { ...req.headers, Authorization: `Bearer ${token}`, }, - body: req.body, + body: req.body as string | Buffer | Uint8Array | undefined, }) return await transformResponse(response) } finally { - await agiloftLogout(params.instanceUrl, params.knowledgeBase, token) + await agiloftLogout(params.instanceUrl, params.knowledgeBase, token, resolvedIP) } } /** * Login helper exported for use in the attach file API route. */ -export { agiloftLogin, agiloftLogout } +export { agiloftLogin, agiloftLogout, validateInstanceUrl } /** URL builders (credential-free -- auth is via Bearer token header) */ diff --git a/apps/sim/tools/apify/run_actor_async.ts b/apps/sim/tools/apify/run_actor_async.ts index ca600e11d20..b569d0bf8eb 100644 --- a/apps/sim/tools/apify/run_actor_async.ts +++ b/apps/sim/tools/apify/run_actor_async.ts @@ -1,4 +1,5 @@ import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { sleep } from '@/lib/core/utils/helpers' import type { RunActorParams, RunActorResult } from '@/tools/apify/types' import type { ToolConfig } from '@/tools/types' @@ -137,7 +138,7 @@ export const apifyRunActorAsyncTool: ToolConfig let elapsedTime = 0 while (elapsedTime < MAX_POLL_TIME_MS) { - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) elapsedTime += POLL_INTERVAL_MS const encodedActorId = encodeURIComponent(params.actorId) diff --git a/apps/sim/tools/brightdata/discover.ts b/apps/sim/tools/brightdata/discover.ts index 153bf465c6e..bfcd5f4522b 100644 --- a/apps/sim/tools/brightdata/discover.ts +++ b/apps/sim/tools/brightdata/discover.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { sleep, toError } from '@/lib/core/utils/helpers' import type { BrightDataDiscoverParams, BrightDataDiscoverResponse } from '@/tools/brightdata/types' import type { ToolConfig } from '@/tools/types' @@ -178,18 +179,18 @@ export const brightDataDiscoverTool: ToolConfig< } } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) elapsedTime += POLL_INTERVAL_MS } catch (error) { logger.error('Error polling for discover task:', { - message: error instanceof Error ? error.message : String(error), + message: toError(error).message, taskId, }) return { ...result, success: false, - error: `Error polling for discover task: ${error instanceof Error ? error.message : String(error)}`, + error: `Error polling for discover task: ${toError(error).message}`, } } } diff --git a/apps/sim/tools/browser_use/run_task.ts b/apps/sim/tools/browser_use/run_task.ts index 9422282a009..ea880fc0314 100644 --- a/apps/sim/tools/browser_use/run_task.ts +++ b/apps/sim/tools/browser_use/run_task.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' +import { sleep } from '@/lib/core/utils/helpers' import type { BrowserUseRunTaskParams, BrowserUseRunTaskResponse } from '@/tools/browser_use/types' import type { ToolConfig, ToolResponse } from '@/tools/types' @@ -165,7 +166,7 @@ async function pollForCompletion( } } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) continue } @@ -188,7 +189,7 @@ async function pollForCompletion( liveUrlLogged = true } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) } const finalResult = await fetchTaskStatus(taskId, apiKey) diff --git a/apps/sim/tools/exa/research.ts b/apps/sim/tools/exa/research.ts index 8af21576f7d..502dbe1facf 100644 --- a/apps/sim/tools/exa/research.ts +++ b/apps/sim/tools/exa/research.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { sleep } from '@/lib/core/utils/helpers' import type { ExaResearchParams, ExaResearchResponse } from '@/tools/exa/types' import type { ToolConfig } from '@/tools/types' @@ -123,7 +124,7 @@ export const researchTool: ToolConfig = } } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) elapsedTime += POLL_INTERVAL_MS } catch (error: any) { logger.error('Error polling for research task status:', { diff --git a/apps/sim/tools/extend/parser.ts b/apps/sim/tools/extend/parser.ts index 4e7dab956b5..d36efcf8bc9 100644 --- a/apps/sim/tools/extend/parser.ts +++ b/apps/sim/tools/extend/parser.ts @@ -1,3 +1,4 @@ +import { toError } from '@/lib/core/utils/helpers' import { isInternalFileUrl } from '@/lib/uploads/utils/file-utils' import type { ExtendParserInput, @@ -108,7 +109,7 @@ export const extendParserTool: ToolConfig ) } } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error( `Invalid URL format: ${errorMessage}. Please provide a valid HTTP or HTTPS URL to a document.` ) diff --git a/apps/sim/tools/firecrawl/agent.ts b/apps/sim/tools/firecrawl/agent.ts index 1a8dc959bf2..07c79a6b2f2 100644 --- a/apps/sim/tools/firecrawl/agent.ts +++ b/apps/sim/tools/firecrawl/agent.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { sleep } from '@/lib/core/utils/helpers' import type { AgentParams, AgentResponse } from '@/tools/firecrawl/types' import type { ToolConfig } from '@/tools/types' @@ -150,7 +151,7 @@ export const agentTool: ToolConfig = { } } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) elapsedTime += POLL_INTERVAL_MS } catch (error: any) { logger.error('Error polling for agent job status:', { diff --git a/apps/sim/tools/firecrawl/crawl.ts b/apps/sim/tools/firecrawl/crawl.ts index bb2eae5b17b..fbc001a809a 100644 --- a/apps/sim/tools/firecrawl/crawl.ts +++ b/apps/sim/tools/firecrawl/crawl.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { sleep } from '@/lib/core/utils/helpers' import type { FirecrawlCrawlParams, FirecrawlCrawlResponse } from '@/tools/firecrawl/types' import { CRAWLED_PAGE_OUTPUT_PROPERTIES } from '@/tools/firecrawl/types' import type { ToolConfig } from '@/tools/types' @@ -193,7 +194,7 @@ export const crawlTool: ToolConfig } } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) elapsedTime += POLL_INTERVAL_MS } catch (error: any) { logger.error('Error polling for crawl job status:', { diff --git a/apps/sim/tools/firecrawl/extract.ts b/apps/sim/tools/firecrawl/extract.ts index db41780af15..b215413acda 100644 --- a/apps/sim/tools/firecrawl/extract.ts +++ b/apps/sim/tools/firecrawl/extract.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { sleep } from '@/lib/core/utils/helpers' import type { ExtractParams, ExtractResponse } from '@/tools/firecrawl/types' import type { ToolConfig } from '@/tools/types' @@ -205,7 +206,7 @@ export const extractTool: ToolConfig = { } } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) elapsedTime += POLL_INTERVAL_MS } catch (error: any) { logger.error('Error polling for extract job status:', { diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index e95b286b4fe..c6f4b521a4a 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -9,6 +9,7 @@ import { validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' import { PlatformEvents } from '@/lib/core/telemetry' +import { sleep, toError } from '@/lib/core/utils/helpers' import { generateRequestId } from '@/lib/core/utils/request' import { getBaseUrl, getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { isUserFile } from '@/lib/core/utils/user-file' @@ -376,7 +377,7 @@ async function executeWithRetry( logger.warn( `[${requestId}] Rate limited for ${toolId} (${envVarName}), retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})` ) - await new Promise((resolve) => setTimeout(resolve, delayMs)) + await sleep(delayMs) } } @@ -652,7 +653,7 @@ function isBodySizeLimitError(errorMessage: string): boolean { * @returns false if not a size limit error (caller should continue handling) */ function handleBodySizeLimitError(error: unknown, requestId: string, context: string): boolean { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message if (isBodySizeLimitError(errorMessage)) { logger.error(`[${requestId}] Request body size limit exceeded for ${context}:`, { @@ -937,7 +938,7 @@ export async function executeTool( if (contextParams.workflowId) contextParams.workflowId = undefined } catch (error: any) { logger.error(`[${requestId}] Error fetching access token for ${toolId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) throw error } @@ -955,7 +956,7 @@ export async function executeTool( finalResult = await tool.postProcess(result, contextParams, executeTool) } catch (error) { logger.error(`[${requestId}] Post-processing error for ${toolId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) finalResult = result } @@ -1010,7 +1011,7 @@ export async function executeTool( finalResult = await tool.postProcess(result, contextParams, executeTool) } catch (error) { logger.error(`[${requestId}] Post-processing error for ${toolId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) finalResult = result } @@ -1047,7 +1048,7 @@ export async function executeTool( } } catch (error: any) { logger.error(`[${requestId}] Error executing tool ${toolId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, stack: error instanceof Error ? error.stack : undefined, }) @@ -1331,8 +1332,7 @@ async function executeToolRequest( ) } catch (validationError) { logger.error(`[${requestId}] Custom tool validation failed for ${toolId}:`, { - error: - validationError instanceof Error ? validationError.message : String(validationError), + error: toError(validationError).message, }) throw validationError } @@ -1540,7 +1540,7 @@ async function executeToolRequest( responseData = await response.json() } catch (jsonError) { logger.error(`[${requestId}] JSON parse error for ${toolId}:`, { - error: jsonError instanceof Error ? jsonError.message : String(jsonError), + error: toError(jsonError).message, }) throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`) } @@ -1582,7 +1582,7 @@ async function executeToolRequest( return data } catch (transformError) { logger.error(`[${requestId}] Transform response error for ${toolId}:`, { - error: transformError instanceof Error ? transformError.message : String(transformError), + error: toError(transformError).message, }) throw transformError } @@ -1599,7 +1599,7 @@ async function executeToolRequest( handleBodySizeLimitError(error, requestId, toolId) logger.error(`[${requestId}] Internal request error for ${toolId}:`, { - error: error instanceof Error ? error.message : String(error), + error: toError(error).message, }) // Let the error bubble up to be handled in the main executeTool function @@ -1867,7 +1867,7 @@ async function executeMcpTool( const duration = endTime.getTime() - new Date(actualStartTime).getTime() // Check if this is a body size limit error - const errorMsg = error instanceof Error ? error.message : String(error) + const errorMsg = toError(error).message if (isBodySizeLimitError(errorMsg)) { logger.error(`[${actualRequestId}] Request body size limit exceeded for mcp:${toolId}:`, { originalError: errorMsg, diff --git a/apps/sim/tools/mistral/parser.ts b/apps/sim/tools/mistral/parser.ts index c4277dd5604..778b4e66c6b 100644 --- a/apps/sim/tools/mistral/parser.ts +++ b/apps/sim/tools/mistral/parser.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import { isInternalFileUrl } from '@/lib/uploads/utils/file-utils' import type { MistralParserInput, @@ -158,7 +159,7 @@ export const mistralParserTool: ToolConfig = ) } } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = toError(error).message throw new Error( `Invalid URL format: ${errorMessage}. Please provide a valid HTTP or HTTPS URL to a document` ) @@ -168,9 +169,7 @@ export const pulseParserTool: ToolConfig = try { parseResult = await response.json() } catch (jsonError) { - throw new Error( - `Failed to parse Pulse response: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}` - ) + throw new Error(`Failed to parse Pulse response: ${toError(jsonError).message}`) } if (!parseResult || typeof parseResult !== 'object') { diff --git a/apps/sim/tools/reducto/parser.ts b/apps/sim/tools/reducto/parser.ts index 44e53bc7c85..adba9c1975c 100644 --- a/apps/sim/tools/reducto/parser.ts +++ b/apps/sim/tools/reducto/parser.ts @@ -1,3 +1,4 @@ +import { toError } from '@/lib/core/utils/helpers' import { isInternalFileUrl } from '@/lib/uploads/utils/file-utils' import type { ReductoParserInput, @@ -102,7 +103,7 @@ export const reductoParserTool: ToolConfig = { @@ -49,7 +50,7 @@ export const countTool: ToolConfig = request: { url: (params) => { - let url = `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*` + let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${params.table}?select=*` // Add filters if provided if (params.filter?.trim()) { diff --git a/apps/sim/tools/supabase/delete.ts b/apps/sim/tools/supabase/delete.ts index f5ae4c53c09..a76a1781b1c 100644 --- a/apps/sim/tools/supabase/delete.ts +++ b/apps/sim/tools/supabase/delete.ts @@ -1,4 +1,5 @@ import type { SupabaseDeleteParams, SupabaseDeleteResponse } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const deleteTool: ToolConfig = { @@ -44,7 +45,7 @@ export const deleteTool: ToolConfig { // Construct the URL for the Supabase REST API with select to return deleted data - let url = `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*` + let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${params.table}?select=*` // Add filters (required for delete) - using PostgREST syntax if (params.filter?.trim()) { diff --git a/apps/sim/tools/supabase/get_row.ts b/apps/sim/tools/supabase/get_row.ts index d65d8ae83b9..e21414dee2c 100644 --- a/apps/sim/tools/supabase/get_row.ts +++ b/apps/sim/tools/supabase/get_row.ts @@ -1,4 +1,5 @@ import type { SupabaseGetRowParams, SupabaseGetRowResponse } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const getRowTool: ToolConfig = { @@ -51,7 +52,7 @@ export const getRowTool: ToolConfig { // Construct the URL for the Supabase REST API const selectColumns = params.select?.trim() || '*' - let url = `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=${encodeURIComponent(selectColumns)}` + let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${params.table}?select=${encodeURIComponent(selectColumns)}` // Add filters (required for get_row) - using PostgREST syntax if (params.filter?.trim()) { diff --git a/apps/sim/tools/supabase/insert.ts b/apps/sim/tools/supabase/insert.ts index 2a93034e889..9cd33696536 100644 --- a/apps/sim/tools/supabase/insert.ts +++ b/apps/sim/tools/supabase/insert.ts @@ -1,4 +1,5 @@ import type { SupabaseInsertParams, SupabaseInsertResponse } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const insertTool: ToolConfig = { @@ -42,7 +43,7 @@ export const insertTool: ToolConfig `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*`, + url: (params) => `${supabaseBaseUrl(params.projectId)}/rest/v1/${params.table}?select=*`, method: 'POST', headers: (params) => { const headers: Record = { diff --git a/apps/sim/tools/supabase/introspect.ts b/apps/sim/tools/supabase/introspect.ts index 064da674083..700a11b733e 100644 --- a/apps/sim/tools/supabase/introspect.ts +++ b/apps/sim/tools/supabase/introspect.ts @@ -6,6 +6,7 @@ import { type SupabaseIntrospectResponse, type SupabaseTableSchema, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SupabaseIntrospect') @@ -335,7 +336,7 @@ export const introspectTool: ToolConfig { - return `https://${params.projectId}.supabase.co/rest/v1/rpc/` + return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/` }, method: 'POST', headers: (params) => ({ @@ -353,8 +354,9 @@ export const introspectTool: ToolConfig = { @@ -69,7 +70,7 @@ export const queryTool: ToolConfig = url: (params) => { // Construct the URL for the Supabase REST API const selectColumns = params.select?.trim() || '*' - let url = `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=${encodeURIComponent(selectColumns)}` + let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${params.table}?select=${encodeURIComponent(selectColumns)}` // Add filters if provided - using PostgREST syntax if (params.filter?.trim()) { diff --git a/apps/sim/tools/supabase/rpc.ts b/apps/sim/tools/supabase/rpc.ts index a57d36a158d..d05271a7b3a 100644 --- a/apps/sim/tools/supabase/rpc.ts +++ b/apps/sim/tools/supabase/rpc.ts @@ -1,4 +1,5 @@ import type { SupabaseRpcParams, SupabaseRpcResponse } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const rpcTool: ToolConfig = { @@ -36,7 +37,7 @@ export const rpcTool: ToolConfig = { request: { url: (params) => { - return `https://${params.projectId}.supabase.co/rest/v1/rpc/${params.functionName}` + return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${params.functionName}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_copy.ts b/apps/sim/tools/supabase/storage_copy.ts index 5f5cad83d15..103f242ab4f 100644 --- a/apps/sim/tools/supabase/storage_copy.ts +++ b/apps/sim/tools/supabase/storage_copy.ts @@ -3,6 +3,7 @@ import { type SupabaseStorageCopyParams, type SupabaseStorageCopyResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageCopyTool: ToolConfig = { @@ -46,7 +47,7 @@ export const storageCopyTool: ToolConfig { - return `https://${params.projectId}.supabase.co/storage/v1/object/copy` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/copy` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_create_bucket.ts b/apps/sim/tools/supabase/storage_create_bucket.ts index 49d33ead7b0..88393042742 100644 --- a/apps/sim/tools/supabase/storage_create_bucket.ts +++ b/apps/sim/tools/supabase/storage_create_bucket.ts @@ -3,6 +3,7 @@ import { type SupabaseStorageCreateBucketParams, type SupabaseStorageCreateBucketResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageCreateBucketTool: ToolConfig< @@ -55,7 +56,7 @@ export const storageCreateBucketTool: ToolConfig< request: { url: (params) => { - return `https://${params.projectId}.supabase.co/storage/v1/bucket` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_create_signed_url.ts b/apps/sim/tools/supabase/storage_create_signed_url.ts index 68e77c0b070..93153af43db 100644 --- a/apps/sim/tools/supabase/storage_create_signed_url.ts +++ b/apps/sim/tools/supabase/storage_create_signed_url.ts @@ -2,6 +2,7 @@ import type { SupabaseStorageCreateSignedUrlParams, SupabaseStorageCreateSignedUrlResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageCreateSignedUrlTool: ToolConfig< @@ -54,7 +55,7 @@ export const storageCreateSignedUrlTool: ToolConfig< request: { url: (params) => { - return `https://${params.projectId}.supabase.co/storage/v1/object/sign/${params.bucket}/${params.path}` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/sign/${params.bucket}/${params.path}` }, method: 'POST', headers: (params) => ({ @@ -84,7 +85,10 @@ export const storageCreateSignedUrlTool: ToolConfig< } const relativePath = data.signedURL || data.signedUrl - const fullUrl = `https://${params?.projectId}.supabase.co/storage/v1${relativePath}` + if (!params?.projectId) { + throw new Error('projectId is required to construct the signed URL') + } + const fullUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1${relativePath}` return { success: true, diff --git a/apps/sim/tools/supabase/storage_delete.ts b/apps/sim/tools/supabase/storage_delete.ts index 37dbe52b911..e0228bb1810 100644 --- a/apps/sim/tools/supabase/storage_delete.ts +++ b/apps/sim/tools/supabase/storage_delete.ts @@ -3,6 +3,7 @@ import { type SupabaseStorageDeleteParams, type SupabaseStorageDeleteResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageDeleteTool: ToolConfig< @@ -43,7 +44,7 @@ export const storageDeleteTool: ToolConfig< request: { url: (params) => { - return `https://${params.projectId}.supabase.co/storage/v1/object/${params.bucket}` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/${params.bucket}` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_delete_bucket.ts b/apps/sim/tools/supabase/storage_delete_bucket.ts index e3a97b2fa35..45764b9c5c8 100644 --- a/apps/sim/tools/supabase/storage_delete_bucket.ts +++ b/apps/sim/tools/supabase/storage_delete_bucket.ts @@ -3,6 +3,7 @@ import { type SupabaseStorageDeleteBucketParams, type SupabaseStorageDeleteBucketResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageDeleteBucketTool: ToolConfig< @@ -37,7 +38,7 @@ export const storageDeleteBucketTool: ToolConfig< request: { url: (params) => { - return `https://${params.projectId}.supabase.co/storage/v1/bucket/${params.bucket}` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket/${params.bucket}` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_download.ts b/apps/sim/tools/supabase/storage_download.ts index d9fc2f9f2ab..d47977f61d8 100644 --- a/apps/sim/tools/supabase/storage_download.ts +++ b/apps/sim/tools/supabase/storage_download.ts @@ -4,6 +4,7 @@ import { type SupabaseStorageDownloadParams, type SupabaseStorageDownloadResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SupabaseStorageDownloadTool') @@ -52,7 +53,7 @@ export const storageDownloadTool: ToolConfig< request: { url: (params) => { - return `https://${params.projectId}.supabase.co/storage/v1/object/${params.bucket}/${params.path}` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/${params.bucket}/${params.path}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_get_public_url.ts b/apps/sim/tools/supabase/storage_get_public_url.ts index 86ebf6dc050..1d843220380 100644 --- a/apps/sim/tools/supabase/storage_get_public_url.ts +++ b/apps/sim/tools/supabase/storage_get_public_url.ts @@ -2,6 +2,7 @@ import type { SupabaseStorageGetPublicUrlParams, SupabaseStorageGetPublicUrlResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageGetPublicUrlTool: ToolConfig< @@ -47,7 +48,7 @@ export const storageGetPublicUrlTool: ToolConfig< }, request: { - url: (params) => `https://${params.projectId}.supabase.co/storage/v1/bucket/${params.bucket}`, + url: (params) => `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket/${params.bucket}`, method: 'GET', headers: (params) => ({ apikey: params.apiKey, @@ -56,7 +57,10 @@ export const storageGetPublicUrlTool: ToolConfig< }, transformResponse: async (response: Response, params?: SupabaseStorageGetPublicUrlParams) => { - let publicUrl = `https://${params?.projectId}.supabase.co/storage/v1/object/public/${params?.bucket}/${params?.path}` + if (!params?.projectId) { + throw new Error('projectId is required to construct the public URL') + } + let publicUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${params.bucket}/${params.path}` if (params?.download) { publicUrl += '?download=true' diff --git a/apps/sim/tools/supabase/storage_list.ts b/apps/sim/tools/supabase/storage_list.ts index 91521c9aee5..fc7599156da 100644 --- a/apps/sim/tools/supabase/storage_list.ts +++ b/apps/sim/tools/supabase/storage_list.ts @@ -3,6 +3,7 @@ import { type SupabaseStorageListParams, type SupabaseStorageListResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageListTool: ToolConfig = { @@ -70,7 +71,7 @@ export const storageListTool: ToolConfig { - return `https://${params.projectId}.supabase.co/storage/v1/object/list/${params.bucket}` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/list/${params.bucket}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_list_buckets.ts b/apps/sim/tools/supabase/storage_list_buckets.ts index bc3db920d42..a7ede783ca4 100644 --- a/apps/sim/tools/supabase/storage_list_buckets.ts +++ b/apps/sim/tools/supabase/storage_list_buckets.ts @@ -3,6 +3,7 @@ import { type SupabaseStorageListBucketsParams, type SupabaseStorageListBucketsResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageListBucketsTool: ToolConfig< @@ -31,7 +32,7 @@ export const storageListBucketsTool: ToolConfig< request: { url: (params) => { - return `https://${params.projectId}.supabase.co/storage/v1/bucket` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_move.ts b/apps/sim/tools/supabase/storage_move.ts index 9fcb225541a..5f3c2edf30a 100644 --- a/apps/sim/tools/supabase/storage_move.ts +++ b/apps/sim/tools/supabase/storage_move.ts @@ -3,6 +3,7 @@ import { type SupabaseStorageMoveParams, type SupabaseStorageMoveResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageMoveTool: ToolConfig = { @@ -46,7 +47,7 @@ export const storageMoveTool: ToolConfig { - return `https://${params.projectId}.supabase.co/storage/v1/object/move` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/move` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/text_search.ts b/apps/sim/tools/supabase/text_search.ts index ee7581493c8..5a813c0c7a7 100644 --- a/apps/sim/tools/supabase/text_search.ts +++ b/apps/sim/tools/supabase/text_search.ts @@ -1,4 +1,5 @@ import type { SupabaseTextSearchParams, SupabaseTextSearchResponse } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const textSearchTool: ToolConfig = { @@ -77,7 +78,7 @@ export const textSearchTool: ToolConfig = { @@ -50,7 +51,7 @@ export const updateTool: ToolConfig { // Construct the URL for the Supabase REST API with select to return updated data - let url = `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*` + let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${params.table}?select=*` // Add filters (required for update) - using PostgREST syntax if (params.filter?.trim()) { diff --git a/apps/sim/tools/supabase/upsert.ts b/apps/sim/tools/supabase/upsert.ts index f275009d9f8..d7ebf41a7e5 100644 --- a/apps/sim/tools/supabase/upsert.ts +++ b/apps/sim/tools/supabase/upsert.ts @@ -1,4 +1,5 @@ import type { SupabaseUpsertParams, SupabaseUpsertResponse } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const upsertTool: ToolConfig = { @@ -42,7 +43,7 @@ export const upsertTool: ToolConfig `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*`, + url: (params) => `${supabaseBaseUrl(params.projectId)}/rest/v1/${params.table}?select=*`, method: 'POST', headers: (params) => { const headers: Record = { diff --git a/apps/sim/tools/supabase/utils.test.ts b/apps/sim/tools/supabase/utils.test.ts new file mode 100644 index 00000000000..18c01fd8bf3 --- /dev/null +++ b/apps/sim/tools/supabase/utils.test.ts @@ -0,0 +1,41 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/config/feature-flags', () => ({ + isHosted: false, +})) + +import { supabaseBaseUrl } from '@/tools/supabase/utils' + +describe('supabaseBaseUrl', () => { + it.concurrent('should return the correct URL for a valid project ID', () => { + const url = supabaseBaseUrl('jdrkgepadsdopsntdlom') + expect(url).toBe('https://jdrkgepadsdopsntdlom.supabase.co') + }) + + it.concurrent('should throw on fragment injection attempt', () => { + expect(() => supabaseBaseUrl('evil#attacker.com')).toThrow() + }) + + it.concurrent('should throw on empty string', () => { + expect(() => supabaseBaseUrl('')).toThrow() + }) + + it.concurrent('should throw on path traversal', () => { + expect(() => supabaseBaseUrl('evil/../../etc')).toThrow() + }) + + it.concurrent('should throw on authority injection', () => { + expect(() => supabaseBaseUrl('evil@attacker.com')).toThrow() + }) + + it.concurrent('should throw on uppercase letters', () => { + expect(() => supabaseBaseUrl('ABCDEFGHIJKLMNOPQRST')).toThrow() + }) + + it.concurrent('should throw on too-short IDs', () => { + expect(() => supabaseBaseUrl('abc')).toThrow() + }) +}) diff --git a/apps/sim/tools/supabase/utils.ts b/apps/sim/tools/supabase/utils.ts new file mode 100644 index 00000000000..d862399f4f9 --- /dev/null +++ b/apps/sim/tools/supabase/utils.ts @@ -0,0 +1,14 @@ +import { validateSupabaseProjectId } from '@/lib/core/security/input-validation' + +/** + * Returns the validated Supabase REST API base URL for a given project ID. + * Throws if the project ID contains characters that could alter the URL + * (e.g. `#`, `/`, `@`), preventing SSRF via fragment injection. + */ +export function supabaseBaseUrl(projectId: string): string { + const result = validateSupabaseProjectId(projectId) + if (!result.isValid) { + throw new Error(result.error) + } + return `https://${result.sanitized}.supabase.co` +} diff --git a/apps/sim/tools/supabase/vector_search.ts b/apps/sim/tools/supabase/vector_search.ts index 51a461ca78f..ba9a3a1b8ae 100644 --- a/apps/sim/tools/supabase/vector_search.ts +++ b/apps/sim/tools/supabase/vector_search.ts @@ -2,6 +2,7 @@ import type { SupabaseVectorSearchParams, SupabaseVectorSearchResponse, } from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const vectorSearchTool: ToolConfig< @@ -56,7 +57,7 @@ export const vectorSearchTool: ToolConfig< request: { url: (params) => { // Use RPC endpoint for calling PostgreSQL functions - return `https://${params.projectId}.supabase.co/rest/v1/rpc/${params.functionName}` + return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${params.functionName}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/textract/parser.ts b/apps/sim/tools/textract/parser.ts index d1a82a5f1e6..65fd98b6fa8 100644 --- a/apps/sim/tools/textract/parser.ts +++ b/apps/sim/tools/textract/parser.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@/lib/core/utils/helpers' import type { TextractParserInput, TextractParserOutput, @@ -139,9 +140,7 @@ export const textractParserTool: ToolConfig = } catch (parseError) { logger.error('Failed to parse JSON response', { contentType, - parseError: parseError instanceof Error ? parseError.message : String(parseError), + parseError: toError(parseError).message, }) throw new Error( `Invalid JSON response: ${parseError instanceof Error ? parseError.message : 'Parse error'}` From df416cc098b5a0f75d6bbd301332df8a124289b8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Apr 2026 12:51:14 -0700 Subject: [PATCH 2/8] fix(agiloft): remove import type from .server module to fix client bundle build Turbopack resolves .server.ts modules even for type-only imports, pulling dns/promises into client bundles. Define SecureFetchResponse locally instead. --- apps/sim/tools/agiloft/utils.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/sim/tools/agiloft/utils.ts b/apps/sim/tools/agiloft/utils.ts index 65c9deff49e..c6e989054e2 100644 --- a/apps/sim/tools/agiloft/utils.ts +++ b/apps/sim/tools/agiloft/utils.ts @@ -1,5 +1,4 @@ import { createLogger } from '@sim/logger' -import type { SecureFetchResponse } from '@/lib/core/security/input-validation.server' import type { AgiloftAttachmentInfoParams, AgiloftBaseParams, @@ -14,6 +13,21 @@ import type { } from '@/tools/agiloft/types' import type { HttpMethod, ToolResponse } from '@/tools/types' +/** + * Mirrors the shape of SecureFetchResponse from input-validation.server.ts. + * Defined locally to avoid importing the .server module into client bundles + * (it pulls in dns/promises which is Node-only). + */ +interface SecureFetchResponse { + ok: boolean + status: number + statusText: string + headers: { get(name: string): string | null } + text: () => Promise + json: () => Promise + arrayBuffer: () => Promise +} + const logger = createLogger('AgiloftAuth') /** From 5d986b22ba09d6f7c3857cbd5a3c0f6b9fb99fc0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Apr 2026 13:02:43 -0700 Subject: [PATCH 3/8] fix(agiloft): revert to client-safe imports to fix build The SSRF upgrade to input-validation.server introduced dns/promises into client bundles via tools/registry.ts. Revert to the original client-safe validateExternalUrl + fetch. The SSRF DNS-pinning upgrade for agiloft directExecution should be done via API routes in a separate PR. --- .../sim/app/api/tools/agiloft/attach/route.ts | 23 ++--- apps/sim/tools/agiloft/utils.ts | 86 +++++-------------- 2 files changed, 31 insertions(+), 78 deletions(-) diff --git a/apps/sim/app/api/tools/agiloft/attach/route.ts b/apps/sim/app/api/tools/agiloft/attach/route.ts index 27c704f6979..db55283d82a 100644 --- a/apps/sim/app/api/tools/agiloft/attach/route.ts +++ b/apps/sim/app/api/tools/agiloft/attach/route.ts @@ -2,17 +2,12 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' -import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server' +import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { FileInputSchema, type RawFileInput } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' -import { - agiloftLogin, - agiloftLogout, - buildAttachFileUrl, - validateInstanceUrl, -} from '@/tools/agiloft/utils' +import { agiloftLogin, agiloftLogout, buildAttachFileUrl } from '@/tools/agiloft/utils' export const dynamic = 'force-dynamic' @@ -65,20 +60,18 @@ export async function POST(request: NextRequest) { const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) const resolvedFileName = data.fileName || userFile.name || 'attachment' - let resolvedIP: string - try { - resolvedIP = await validateInstanceUrl(data.instanceUrl) - } catch (error) { + const urlValidation = await validateUrlWithDNS(data.instanceUrl, 'instanceUrl') + if (!urlValidation.isValid) { logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, { instanceUrl: data.instanceUrl, }) return NextResponse.json( - { success: false, error: error instanceof Error ? error.message : 'Invalid instance URL' }, + { success: false, error: urlValidation.error || 'Invalid instance URL' }, { status: 400 } ) } - const token = await agiloftLogin(data, resolvedIP) + const token = await agiloftLogin(data) const base = data.instanceUrl.replace(/\/$/, '') try { @@ -86,7 +79,7 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Uploading file to Agiloft: ${resolvedFileName}`) - const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, { + const agiloftResponse = await fetch(url, { method: 'PUT', headers: { 'Content-Type': userFile.type || 'application/octet-stream', @@ -130,7 +123,7 @@ export async function POST(request: NextRequest) { }, }) } finally { - await agiloftLogout(data.instanceUrl, data.knowledgeBase, token, resolvedIP) + await agiloftLogout(data.instanceUrl, data.knowledgeBase, token) } } catch (error) { if (error instanceof z.ZodError) { diff --git a/apps/sim/tools/agiloft/utils.ts b/apps/sim/tools/agiloft/utils.ts index c6e989054e2..252dcb4a819 100644 --- a/apps/sim/tools/agiloft/utils.ts +++ b/apps/sim/tools/agiloft/utils.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { validateExternalUrl } from '@/lib/core/security/input-validation' import type { AgiloftAttachmentInfoParams, AgiloftBaseParams, @@ -13,35 +14,8 @@ import type { } from '@/tools/agiloft/types' import type { HttpMethod, ToolResponse } from '@/tools/types' -/** - * Mirrors the shape of SecureFetchResponse from input-validation.server.ts. - * Defined locally to avoid importing the .server module into client bundles - * (it pulls in dns/promises which is Node-only). - */ -interface SecureFetchResponse { - ok: boolean - status: number - statusText: string - headers: { get(name: string): string | null } - text: () => Promise - json: () => Promise - arrayBuffer: () => Promise -} - const logger = createLogger('AgiloftAuth') -/** - * Lazily imports server-only security functions to avoid pulling `dns/promises` - * into client bundles (this file is reachable from tools/registry.ts). - */ -async function getServerSecurity() { - const mod = await import('@/lib/core/security/input-validation.server') - return { - secureFetchWithPinnedIP: mod.secureFetchWithPinnedIP, - validateUrlWithDNS: mod.validateUrlWithDNS, - } -} - interface AgiloftRequestConfig { url: string method: HttpMethod @@ -49,40 +23,30 @@ interface AgiloftRequestConfig { body?: BodyInit } -/** - * Validates the instance URL via DNS resolution and returns the resolved IP - * for use with pinned fetches to prevent SSRF via DNS rebinding. - */ -async function validateInstanceUrl(instanceUrl: string): Promise { - const { validateUrlWithDNS } = await getServerSecurity() - const validation = await validateUrlWithDNS(instanceUrl, 'instanceUrl') - if (!validation.isValid) { - throw new Error(`Invalid Agiloft instance URL: ${validation.error}`) - } - return validation.resolvedIP! -} - /** * Exchanges login/password for a short-lived Bearer token via EWLogin. - * Uses DNS-pinned fetch to prevent SSRF via DNS rebinding. */ -async function agiloftLogin(params: AgiloftBaseParams, resolvedIP: string): Promise { +async function agiloftLogin(params: AgiloftBaseParams): Promise { const base = params.instanceUrl.replace(/\/$/, '') + const urlValidation = validateExternalUrl(params.instanceUrl, 'instanceUrl') + if (!urlValidation.isValid) { + throw new Error(`Invalid Agiloft instance URL: ${urlValidation.error}`) + } + const kb = encodeURIComponent(params.knowledgeBase) const login = encodeURIComponent(params.login) const password = encodeURIComponent(params.password) const url = `${base}/ewws/EWLogin?$KB=${kb}&$login=${login}&$password=${password}` - const { secureFetchWithPinnedIP } = await getServerSecurity() - const response = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'POST' }) + const response = await fetch(url, { method: 'POST' }) if (!response.ok) { const errorText = await response.text() throw new Error(`Agiloft login failed: ${response.status} - ${errorText}`) } - const data = (await response.json()) as { access_token?: string } + const data = await response.json() const token = data.access_token if (!token) { @@ -94,19 +58,16 @@ async function agiloftLogin(params: AgiloftBaseParams, resolvedIP: string): Prom /** * Cleans up the server session. Best-effort — failures are logged but not thrown. - * Uses DNS-pinned fetch to prevent SSRF via DNS rebinding. */ async function agiloftLogout( instanceUrl: string, knowledgeBase: string, - token: string, - resolvedIP: string + token: string ): Promise { try { const base = instanceUrl.replace(/\/$/, '') const kb = encodeURIComponent(knowledgeBase) - const { secureFetchWithPinnedIP } = await getServerSecurity() - await secureFetchWithPinnedIP(`${base}/ewws/EWLogout?$KB=${kb}`, resolvedIP, { + await fetch(`${base}/ewws/EWLogout?$KB=${kb}`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }) @@ -117,43 +78,42 @@ async function agiloftLogout( /** * Shared wrapper that handles the full auth lifecycle: - * 1. Validate instance URL via DNS resolution - * 2. Login to get Bearer token (using pinned IP) - * 3. Execute the request with the token (using pinned IP) - * 4. Logout to clean up the session (using pinned IP) + * 1. Login to get Bearer token + * 2. Execute the request with the token + * 3. Logout to clean up the session * - * All HTTP requests use the resolved IP to prevent SSRF via DNS rebinding. + * The `buildRequest` callback receives the token and base URL, and returns + * the request config. The `transformResponse` callback converts the raw + * Response into the tool's output format. */ export async function executeAgiloftRequest( params: AgiloftBaseParams, buildRequest: (base: string) => AgiloftRequestConfig, - transformResponse: (response: SecureFetchResponse) => Promise + transformResponse: (response: Response) => Promise ): Promise { - const resolvedIP = await validateInstanceUrl(params.instanceUrl) - const token = await agiloftLogin(params, resolvedIP) + const token = await agiloftLogin(params) const base = params.instanceUrl.replace(/\/$/, '') try { const req = buildRequest(base) - const { secureFetchWithPinnedIP } = await getServerSecurity() - const response = await secureFetchWithPinnedIP(req.url, resolvedIP, { + const response = await fetch(req.url, { method: req.method, headers: { ...req.headers, Authorization: `Bearer ${token}`, }, - body: req.body as string | Buffer | Uint8Array | undefined, + body: req.body, }) return await transformResponse(response) } finally { - await agiloftLogout(params.instanceUrl, params.knowledgeBase, token, resolvedIP) + await agiloftLogout(params.instanceUrl, params.knowledgeBase, token) } } /** * Login helper exported for use in the attach file API route. */ -export { agiloftLogin, agiloftLogout, validateInstanceUrl } +export { agiloftLogin, agiloftLogout } /** URL builders (credential-free -- auth is via Bearer token header) */ From 5ea64e9dc4fdda99054090f894743e4f05d5cac1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Apr 2026 13:10:50 -0700 Subject: [PATCH 4/8] feat(agiloft): add API route for retrieve_attachment, matching established file patterns Convert retrieve_attachment from directExecution to standard API route pattern, consistent with Slack download and Google Drive download tools. - Create /api/tools/agiloft/retrieve with DNS validation, auth lifecycle, and base64 file response matching the { file: { name, mimeType, data, size } } convention - Update retrieve_attachment tool to use request/transformResponse instead of directExecution, removing the dependency on executeAgiloftRequest from the tool definition - File output type: 'file' enables FileToolProcessor to store downloaded files in execution filesystem automatically --- .../app/api/tools/agiloft/retrieve/route.ts | 134 ++++++++++++++++++ apps/sim/tools/agiloft/retrieve_attachment.ts | 85 +++++------ 2 files changed, 171 insertions(+), 48 deletions(-) create mode 100644 apps/sim/app/api/tools/agiloft/retrieve/route.ts diff --git a/apps/sim/app/api/tools/agiloft/retrieve/route.ts b/apps/sim/app/api/tools/agiloft/retrieve/route.ts new file mode 100644 index 00000000000..44f16ef81cc --- /dev/null +++ b/apps/sim/app/api/tools/agiloft/retrieve/route.ts @@ -0,0 +1,134 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' +import { generateRequestId } from '@/lib/core/utils/request' +import { agiloftLogin, agiloftLogout, buildRetrieveAttachmentUrl } from '@/tools/agiloft/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AgiloftRetrieveAPI') + +const AgiloftRetrieveSchema = z.object({ + instanceUrl: z.string().min(1, 'Instance URL is required'), + knowledgeBase: z.string().min(1, 'Knowledge base is required'), + login: z.string().min(1, 'Login is required'), + password: z.string().min(1, 'Password is required'), + table: z.string().min(1, 'Table is required'), + recordId: z.string().min(1, 'Record ID is required'), + fieldName: z.string().min(1, 'Field name is required'), + position: z.string().min(1, 'Position is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Agiloft retrieve attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const body = await request.json() + const data = AgiloftRetrieveSchema.parse(body) + + const urlValidation = await validateUrlWithDNS(data.instanceUrl, 'instanceUrl') + if (!urlValidation.isValid) { + logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, { + instanceUrl: data.instanceUrl, + }) + return NextResponse.json( + { success: false, error: urlValidation.error || 'Invalid instance URL' }, + { status: 400 } + ) + } + + const token = await agiloftLogin(data) + const base = data.instanceUrl.replace(/\/$/, '') + + try { + const url = buildRetrieveAttachmentUrl(base, data) + + logger.info(`[${requestId}] Downloading attachment from Agiloft`, { + recordId: data.recordId, + fieldName: data.fieldName, + position: data.position, + }) + + const agiloftResponse = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + if (!agiloftResponse.ok) { + const errorText = await agiloftResponse.text() + logger.error( + `[${requestId}] Agiloft retrieve error: ${agiloftResponse.status} - ${errorText}` + ) + return NextResponse.json( + { success: false, error: `Agiloft error: ${agiloftResponse.status} - ${errorText}` }, + { status: agiloftResponse.status } + ) + } + + const contentType = agiloftResponse.headers.get('content-type') || 'application/octet-stream' + const contentDisposition = agiloftResponse.headers.get('content-disposition') + let fileName = 'attachment' + + if (contentDisposition) { + const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) + if (match?.[1]) { + fileName = match[1].replace(/['"]/g, '') + } + } + + const arrayBuffer = await agiloftResponse.arrayBuffer() + const fileBuffer = Buffer.from(arrayBuffer) + + logger.info(`[${requestId}] Attachment downloaded successfully`, { + name: fileName, + size: fileBuffer.length, + mimeType: contentType, + }) + + const base64Data = fileBuffer.toString('base64') + + return NextResponse.json({ + success: true, + output: { + file: { + name: fileName, + mimeType: contentType, + data: base64Data, + size: fileBuffer.length, + }, + }, + }) + } finally { + await agiloftLogout(data.instanceUrl, data.knowledgeBase, token) + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { success: false, error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error retrieving Agiloft attachment:`, error) + + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/tools/agiloft/retrieve_attachment.ts b/apps/sim/tools/agiloft/retrieve_attachment.ts index 721efdc0067..c80ebbfd13b 100644 --- a/apps/sim/tools/agiloft/retrieve_attachment.ts +++ b/apps/sim/tools/agiloft/retrieve_attachment.ts @@ -2,7 +2,6 @@ import type { AgiloftRetrieveAttachmentParams, AgiloftRetrieveAttachmentResponse, } from '@/tools/agiloft/types' -import { buildRetrieveAttachmentUrl, executeAgiloftRequest } from '@/tools/agiloft/utils' import type { ToolConfig } from '@/tools/types' export const agiloftRetrieveAttachmentTool: ToolConfig< @@ -66,57 +65,47 @@ export const agiloftRetrieveAttachmentTool: ToolConfig< }, request: { - url: 'https://placeholder.agiloft.com', - method: 'GET', - headers: () => ({}), + url: '/api/tools/agiloft/retrieve', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + instanceUrl: params.instanceUrl, + knowledgeBase: params.knowledgeBase, + login: params.login, + password: params.password, + table: params.table, + recordId: params.recordId, + fieldName: params.fieldName, + position: params.position, + }), }, - directExecution: async (params) => { - return executeAgiloftRequest( - params, - (base) => ({ - url: buildRetrieveAttachmentUrl(base, params), - method: 'GET', - }), - async (response) => { - if (!response.ok) { - const errorText = await response.text() - return { - success: false, - output: { - file: { name: '', mimeType: '', data: '', size: 0 }, - }, - error: `Agiloft error: ${response.status} - ${errorText}`, - } - } + transformResponse: async (response: Response) => { + const data = await response.json() - const contentType = response.headers.get('content-type') || 'application/octet-stream' - const contentDisposition = response.headers.get('content-disposition') - let fileName = 'attachment' - - if (contentDisposition) { - const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) - if (match?.[1]) { - fileName = match[1].replace(/['"]/g, '') - } - } - - const arrayBuffer = await response.arrayBuffer() - const buffer = Buffer.from(arrayBuffer) - - return { - success: true, - output: { - file: { - name: fileName, - mimeType: contentType, - data: buffer.toString('base64'), - size: buffer.length, - }, - }, - } + if (!data.success) { + return { + success: false, + output: { + file: { name: '', mimeType: '', data: '', size: 0 }, + }, + error: data.error || 'Failed to retrieve attachment', } - ) + } + + return { + success: true, + output: { + file: { + name: data.output.file.name, + mimeType: data.output.file.mimeType, + data: data.output.file.data, + size: data.output.file.size, + }, + }, + } }, outputs: { From f40ccd48a27b92c7984a218e944ad066e61aa754 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Apr 2026 13:12:20 -0700 Subject: [PATCH 5/8] shopify --- apps/sim/app/api/auth/oauth2/shopify/store/route.ts | 3 ++- apps/sim/app/api/auth/shopify/authorize/route.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/auth/oauth2/shopify/store/route.ts b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts index dc01838c4a8..aac20aca780 100644 --- a/apps/sim/app/api/auth/oauth2/shopify/store/route.ts +++ b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts @@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { getBaseUrl } from '@/lib/core/utils/urls' +import { isSameOrigin } from '@/lib/core/utils/validation' import { processCredentialDraft } from '@/lib/credentials/draft-processor' import { safeAccountInsert } from '@/app/api/auth/oauth/utils' @@ -113,7 +114,7 @@ export async function GET(request: NextRequest) { const returnUrl = request.cookies.get('shopify_return_url')?.value - const redirectUrl = returnUrl || `${baseUrl}/workspace` + const redirectUrl = returnUrl && isSameOrigin(returnUrl) ? returnUrl : `${baseUrl}/workspace` const finalUrl = new URL(redirectUrl) finalUrl.searchParams.set('shopify_connected', 'true') diff --git a/apps/sim/app/api/auth/shopify/authorize/route.ts b/apps/sim/app/api/auth/shopify/authorize/route.ts index ed5a58cb3ce..0fec2c90c00 100644 --- a/apps/sim/app/api/auth/shopify/authorize/route.ts +++ b/apps/sim/app/api/auth/shopify/authorize/route.ts @@ -4,6 +4,7 @@ import { getSession } from '@/lib/auth' import { env } from '@/lib/core/config/env' import { getBaseUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' +import { isSameOrigin } from '@/lib/core/utils/validation' import { getScopesForService } from '@/lib/oauth/utils' const logger = createLogger('ShopifyAuthorize') @@ -192,7 +193,7 @@ export async function GET(request: NextRequest) { path: '/', }) - if (returnUrl) { + if (returnUrl && isSameOrigin(returnUrl)) { response.cookies.set('shopify_return_url', returnUrl, { httpOnly: true, secure: process.env.NODE_ENV === 'production', From 1fcc18a326d65b22f015fffbe1e2e5dd1e527140 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Apr 2026 13:17:06 -0700 Subject: [PATCH 6/8] fix(agiloft): add optional flag to nullable lock record block outputs Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/agiloft.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/blocks/blocks/agiloft.ts b/apps/sim/blocks/blocks/agiloft.ts index 36e571dad99..20c16683deb 100644 --- a/apps/sim/blocks/blocks/agiloft.ts +++ b/apps/sim/blocks/blocks/agiloft.ts @@ -397,11 +397,13 @@ export const AgiloftBlock: BlockConfig = { type: 'string', description: 'Username of the user who locked the record', condition: { field: 'operation', value: 'lock_record' }, + optional: true, }, lockExpiresInMinutes: { type: 'number', description: 'Minutes until the lock expires', condition: { field: 'operation', value: 'lock_record' }, + optional: true, }, }, } From 4b89bd620c5e3390f68198dad4e837037857a14a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Apr 2026 13:22:46 -0700 Subject: [PATCH 7/8] =?UTF-8?q?fix(agiloft):=20revert=20optional=20flag=20?= =?UTF-8?q?on=20block=20outputs=20=E2=80=94=20property=20only=20exists=20o?= =?UTF-8?q?n=20tool=20outputs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/agiloft.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/sim/blocks/blocks/agiloft.ts b/apps/sim/blocks/blocks/agiloft.ts index 20c16683deb..36e571dad99 100644 --- a/apps/sim/blocks/blocks/agiloft.ts +++ b/apps/sim/blocks/blocks/agiloft.ts @@ -397,13 +397,11 @@ export const AgiloftBlock: BlockConfig = { type: 'string', description: 'Username of the user who locked the record', condition: { field: 'operation', value: 'lock_record' }, - optional: true, }, lockExpiresInMinutes: { type: 'number', description: 'Minutes until the lock expires', condition: { field: 'operation', value: 'lock_record' }, - optional: true, }, }, } From 1b4d81122f89f46887b7ef44a5cfa9ee464bf663 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Apr 2026 13:28:49 -0700 Subject: [PATCH 8/8] chore(utils): remove unused utilities (asserts, safeJsonParse, isNonNull) Co-Authored-By: Claude Opus 4.6 --- .claude/rules/global.md | 7 ------- .cursor/rules/global.mdc | 7 ------- CLAUDE.md | 2 +- apps/sim/lib/core/utils/asserts.ts | 17 ----------------- apps/sim/lib/core/utils/helpers.ts | 22 ---------------------- 5 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 apps/sim/lib/core/utils/asserts.ts diff --git a/.claude/rules/global.md b/.claude/rules/global.md index 85877cca174..4c98edc16cb 100644 --- a/.claude/rules/global.md +++ b/.claude/rules/global.md @@ -36,13 +36,6 @@ Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline imp - `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))` - `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))` - `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)` -- `safeJsonParse(str, fallback?)` — parse JSON without throwing. Never write `try { JSON.parse(str) } catch { return default }` -- `isNonNull(value)` — type-narrowing filter predicate for null/undefined - -Use assertion utilities from `@/lib/core/utils/asserts`: - -- `invariant(condition, message)` — assert a condition is truthy, throws if not -- `assertNever(value)` — exhaustive switch/if-else check, TypeScript errors at compile time if a case is unhandled ```typescript // ✗ Bad diff --git a/.cursor/rules/global.mdc b/.cursor/rules/global.mdc index f031fdc8b62..991244503f3 100644 --- a/.cursor/rules/global.mdc +++ b/.cursor/rules/global.mdc @@ -43,13 +43,6 @@ Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline imp - `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))` - `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))` - `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)` -- `safeJsonParse(str, fallback?)` — parse JSON without throwing. Never write `try { JSON.parse(str) } catch { return default }` -- `isNonNull(value)` — type-narrowing filter predicate for null/undefined - -Use assertion utilities from `@/lib/core/utils/asserts`: - -- `invariant(condition, message)` — assert a condition is truthy, throws if not -- `assertNever(value)` — exhaustive switch/if-else check, TypeScript errors at compile time if a case is unhandled ```typescript // ✗ Bad diff --git a/CLAUDE.md b/CLAUDE.md index e130c6c9966..965ae7fd7b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ You are a professional software engineer. All code must follow best practices: a - **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments - **Styling**: Never update global styles. Keep all styling local to components - **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid` -- **Common Utilities**: Use shared helpers from `@/lib/core/utils/helpers` instead of inline implementations. `sleep(ms)` for delays, `toError(e)` to normalize caught values, `safeJsonParse(str, fallback?)` for safe JSON parsing, `isNonNull(v)` for type-narrowing null filters. Use `invariant(cond, msg)` and `assertNever(val)` from `@/lib/core/utils/asserts` for runtime assertions and exhaustive checks. +- **Common Utilities**: Use shared helpers from `@/lib/core/utils/helpers` instead of inline implementations. `sleep(ms)` for delays, `toError(e)` to normalize caught values. - **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx` ## Architecture diff --git a/apps/sim/lib/core/utils/asserts.ts b/apps/sim/lib/core/utils/asserts.ts deleted file mode 100644 index 1d463652f1a..00000000000 --- a/apps/sim/lib/core/utils/asserts.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Asserts that a condition is truthy, throwing an Error if it is not. - * Use for invariants that should never be violated at runtime. - */ -export function invariant(condition: unknown, message: string): asserts condition { - if (!condition) { - throw new Error(message) - } -} - -/** - * Asserts that a value is `never`, useful for exhaustive switch/if-else checks. - * TypeScript will error at compile time if a case is unhandled. - */ -export function assertNever(value: never, message?: string): never { - throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`) -} diff --git a/apps/sim/lib/core/utils/helpers.ts b/apps/sim/lib/core/utils/helpers.ts index 40c1a4a6f58..0b952ae5180 100644 --- a/apps/sim/lib/core/utils/helpers.ts +++ b/apps/sim/lib/core/utils/helpers.ts @@ -6,28 +6,6 @@ export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } -/** - * Parses a JSON string, returning a fallback value on failure instead of throwing. - * Replaces the common `try { JSON.parse(str) } catch { return default }` pattern. - */ -export function safeJsonParse(value: string): T | undefined -export function safeJsonParse(value: string, fallback: T): T -export function safeJsonParse(value: string, fallback?: T): T | undefined { - try { - return JSON.parse(value) as T - } catch { - return fallback - } -} - -/** - * Type-safe filter predicate that removes null and undefined values. - * Fixes the common `.filter(Boolean)` pattern which doesn't narrow types in TypeScript. - */ -export function isNonNull(value: T | null | undefined): value is T { - return value != null -} - /** * Normalizes an unknown caught value into an Error instance. * Replaces the common `e instanceof Error ? e : new Error(String(e))` pattern in catch clauses.