diff --git a/.claude/commands/add-trigger.md b/.claude/commands/add-trigger.md index e12eb393ba7..f5990517573 100644 --- a/.claude/commands/add-trigger.md +++ b/.claude/commands/add-trigger.md @@ -423,7 +423,6 @@ export const {service}PollingTrigger: TriggerConfig = { subBlocks: [ { id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true }, // ... service-specific config fields (dropdowns, inputs, switches) ... - { id: 'triggerSave', type: 'trigger-save', title: '', hideFromPreview: true, mode: 'trigger', triggerId: '{service}_poller' }, { id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' }, ], @@ -486,7 +485,6 @@ Add to `helm/sim/values.yaml` under the existing polling cron jobs: - [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts` - [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`) - [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry -- [ ] `triggerSave` subBlock `triggerId` matches trigger config `id` - [ ] First poll seeds state and emits nothing - [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts` - [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts` diff --git a/.claude/rules/constitution.md b/.claude/rules/constitution.md new file mode 100644 index 00000000000..6881c060ee8 --- /dev/null +++ b/.claude/rules/constitution.md @@ -0,0 +1,71 @@ +# Sim — Language & Positioning + +When editing user-facing copy (landing pages, docs, metadata, marketing), follow these rules. + +## Identity + +Sim is the **AI workspace** where teams build and run AI agents. Not a workflow tool, not an agent framework, not an automation platform. + +**Short definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. + +**Full definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. + +## Audience + +**Primary:** Teams building AI agents for their organization — IT, operations, and technical teams who need governance, security, lifecycle management, and collaboration. + +**Secondary:** Individual builders and developers who care about speed, flexibility, and open source. + +## Required Language + +| Concept | Use | Never use | +|---------|-----|-----------| +| The product | "AI workspace" | "workflow tool", "automation platform", "agent framework" | +| Building | "build agents", "create agents" | "create workflows" (unless describing the workflow module specifically) | +| Visual builder | "workflow builder" or "visual builder" | "canvas", "graph editor" | +| Mothership | "Mothership" (capitalized) | "chat", "AI assistant", "copilot" | +| Deployment | "deploy", "ship" | "publish", "activate" | +| Audience | "teams", "builders" | "users", "customers" (in marketing copy) | +| What agents do | "automate real work" | "automate tasks", "automate workflows" | +| Our advantage | "open-source AI workspace" | "open-source platform" | + +## Tone + +- **Direct.** Short sentences. Active voice. Lead with what it does. +- **Concrete.** Name specific things — "Slack bots, compliance agents, data pipelines" — not abstractions. +- **Confident, not loud.** No exclamation marks or superlatives. +- **Simple.** If a 16-year-old can't understand the sentence, rewrite it. + +## Claim Hierarchy + +When describing Sim, always lead with the most differentiated claim: + +1. **What it is:** "The AI workspace for teams" +2. **What you do:** "Build, deploy, and manage AI agents" +3. **How:** "Visually, conversationally, or with code" +4. **Scale:** "1,000+ integrations, every major LLM" +5. **Trust:** "Open source. SOC2. Trusted by 100,000+ builders." + +## Module Descriptions + +| Module | One-liner | +|--------|-----------| +| **Mothership** | Your AI command center. Build and manage everything in natural language. | +| **Workflows** | The visual builder. Connect blocks, models, and integrations into agent logic. | +| **Knowledge Base** | Your agents' memory. Upload docs, sync sources, build vector databases. | +| **Tables** | A database, built in. Store, query, and wire structured data into agent runs. | +| **Files** | Upload, create, and share. One store for your team and every agent. | +| **Logs** | Full visibility, every run. Trace execution block by block. | + +## What We Never Say + +- Never call Sim "just a workflow tool" +- Never compare only on integration count — we win on AI-native capabilities +- Never use "no-code" as the primary descriptor — say "visually, conversationally, or with code" +- Never promise unshipped features +- Never use jargon ("RAG", "vector database", "MCP") without plain-English explanation on public pages +- Avoid "agentic workforce" as a primary term — use "AI agents" + +## Vision + +Sim becomes the default environment where teams build AI agents — not a tool you visit for one task, but a workspace you live in. Workflows are one module; Mothership is another. The workspace is the constant; the interface adapts. diff --git a/.cursor/commands/add-trigger.md b/.cursor/commands/add-trigger.md index ae19f0f295b..6e1e6ed975f 100644 --- a/.cursor/commands/add-trigger.md +++ b/.cursor/commands/add-trigger.md @@ -418,7 +418,6 @@ export const {service}PollingTrigger: TriggerConfig = { subBlocks: [ { id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true }, // ... service-specific config fields (dropdowns, inputs, switches) ... - { id: 'triggerSave', type: 'trigger-save', title: '', hideFromPreview: true, mode: 'trigger', triggerId: '{service}_poller' }, { id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' }, ], @@ -481,7 +480,6 @@ Add to `helm/sim/values.yaml` under the existing polling cron jobs: - [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts` - [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`) - [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry -- [ ] `triggerSave` subBlock `triggerId` matches trigger config `id` - [ ] First poll seeds state and emits nothing - [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts` - [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts` diff --git a/.cursor/rules/constitution.mdc b/.cursor/rules/constitution.mdc new file mode 100644 index 00000000000..94186db6e3a --- /dev/null +++ b/.cursor/rules/constitution.mdc @@ -0,0 +1,76 @@ +--- +description: Sim product language, positioning, and tone guidelines +globs: ["apps/sim/app/(landing)/**", "apps/sim/app/(home)/**", "apps/docs/**", "apps/sim/app/manifest.ts", "apps/sim/app/sitemap.ts", "apps/sim/app/robots.ts", "apps/sim/app/llms.txt/**", "apps/sim/app/llms-full.txt/**", "apps/sim/app/(landing)/**/structured-data*", "apps/docs/**/structured-data*", "**/metadata*", "**/seo*"] +--- + +# Sim — Language & Positioning + +When editing user-facing copy (landing pages, docs, metadata, marketing), follow these rules. + +## Identity + +Sim is the **AI workspace** where teams build and run AI agents. Not a workflow tool, not an agent framework, not an automation platform. + +**Short definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. + +**Full definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. + +## Audience + +**Primary:** Teams building AI agents for their organization — IT, operations, and technical teams who need governance, security, lifecycle management, and collaboration. + +**Secondary:** Individual builders and developers who care about speed, flexibility, and open source. + +## Required Language + +| Concept | Use | Never use | +|---------|-----|-----------| +| The product | "AI workspace" | "workflow tool", "automation platform", "agent framework" | +| Building | "build agents", "create agents" | "create workflows" (unless describing the workflow module specifically) | +| Visual builder | "workflow builder" or "visual builder" | "canvas", "graph editor" | +| Mothership | "Mothership" (capitalized) | "chat", "AI assistant", "copilot" | +| Deployment | "deploy", "ship" | "publish", "activate" | +| Audience | "teams", "builders" | "users", "customers" (in marketing copy) | +| What agents do | "automate real work" | "automate tasks", "automate workflows" | +| Our advantage | "open-source AI workspace" | "open-source platform" | + +## Tone + +- **Direct.** Short sentences. Active voice. Lead with what it does. +- **Concrete.** Name specific things — "Slack bots, compliance agents, data pipelines" — not abstractions. +- **Confident, not loud.** No exclamation marks or superlatives. +- **Simple.** If a 16-year-old can't understand the sentence, rewrite it. + +## Claim Hierarchy + +When describing Sim, always lead with the most differentiated claim: + +1. **What it is:** "The AI workspace for teams" +2. **What you do:** "Build, deploy, and manage AI agents" +3. **How:** "Visually, conversationally, or with code" +4. **Scale:** "1,000+ integrations, every major LLM" +5. **Trust:** "Open source. SOC2. Trusted by 100,000+ builders." + +## Module Descriptions + +| Module | One-liner | +|--------|-----------| +| **Mothership** | Your AI command center. Build and manage everything in natural language. | +| **Workflows** | The visual builder. Connect blocks, models, and integrations into agent logic. | +| **Knowledge Base** | Your agents' memory. Upload docs, sync sources, build vector databases. | +| **Tables** | A database, built in. Store, query, and wire structured data into agent runs. | +| **Files** | Upload, create, and share. One store for your team and every agent. | +| **Logs** | Full visibility, every run. Trace execution block by block. | + +## What We Never Say + +- Never call Sim "just a workflow tool" +- Never compare only on integration count — we win on AI-native capabilities +- Never use "no-code" as the primary descriptor — say "visually, conversationally, or with code" +- Never promise unshipped features +- Never use jargon ("RAG", "vector database", "MCP") without plain-English explanation on public pages +- Avoid "agentic workforce" as a primary term — use "AI agents" + +## Vision + +Sim becomes the default environment where teams build AI agents — not a tool you visit for one task, but a workspace you live in. Workflows are one module; Mothership is another. The workspace is the constant; the interface adapts. diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx index 94b9531ec12..d01cd5d3598 100644 --- a/apps/docs/app/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx @@ -280,12 +280,12 @@ export async function generateMetadata(props: { title: data.title, description: data.description || - 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.', + 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.', keywords: [ 'AI agents', - 'agentic workforce', - 'AI agent platform', - 'agentic workflows', + 'AI workspace', + 'AI agent builder', + 'build AI agents', 'LLM orchestration', 'AI automation', 'knowledge base', @@ -300,7 +300,7 @@ export async function generateMetadata(props: { title: data.title, description: data.description || - 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.', + 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.', url: fullUrl, siteName: 'Sim Documentation', type: 'article', @@ -322,7 +322,7 @@ export async function generateMetadata(props: { title: data.title, description: data.description || - 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.', + 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.', images: [ogImageUrl], creator: '@simdotai', site: '@simdotai', diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx index 5f9ca80cdd1..c500f440cbb 100644 --- a/apps/docs/app/[lang]/layout.tsx +++ b/apps/docs/app/[lang]/layout.tsx @@ -66,7 +66,7 @@ export default async function Layout({ children, params }: LayoutProps) { '@type': 'WebSite', name: 'Sim Documentation', description: - 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.', + 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.', url: 'https://docs.sim.ai', publisher: { '@type': 'Organization', diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index ae83961eabd..80332d4c353 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -14,29 +14,27 @@ export const viewport: Viewport = { export const metadata = { metadataBase: new URL('https://docs.sim.ai'), title: { - default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce', + default: 'Sim Documentation — The AI Workspace for Teams', template: '%s | Sim Docs', }, description: - 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.', + 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.', applicationName: 'Sim Docs', generator: 'Next.js', referrer: 'origin-when-cross-origin' as const, keywords: [ + 'AI workspace', + 'AI agent builder', 'AI agents', - 'agentic workforce', - 'AI agent platform', + 'build AI agents', 'open-source AI agents', - 'agentic workflows', 'LLM orchestration', 'AI integrations', 'knowledge base', 'AI automation', - 'workflow builder', - 'AI workflow orchestration', + 'visual workflow builder', 'enterprise AI', 'AI agent deployment', - 'intelligent automation', 'AI tools', ], authors: [{ name: 'Sim Team', url: 'https://sim.ai' }], @@ -65,9 +63,9 @@ export const metadata = { alternateLocale: ['es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'zh_CN'], url: 'https://docs.sim.ai', siteName: 'Sim Documentation', - title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce', + title: 'Sim Documentation — The AI Workspace for Teams', description: - 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.', + 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.', images: [ { url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation', @@ -79,9 +77,9 @@ export const metadata = { }, twitter: { card: 'summary_large_image', - title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce', + title: 'Sim Documentation — The AI Workspace for Teams', description: - 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.', + 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.', creator: '@simdotai', site: '@simdotai', images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation'], diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts index 352f4cc9043..c47d08d6b9e 100644 --- a/apps/docs/app/llms.txt/route.ts +++ b/apps/docs/app/llms.txt/route.ts @@ -37,9 +37,9 @@ export async function GET() { const manifest = `# Sim Documentation -> The open-source platform to build AI agents and run your agentic workforce. +> The open-source AI workspace where teams build, deploy, and manage AI agents. -Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. +Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders. ## Documentation Overview diff --git a/apps/docs/components/structured-data.tsx b/apps/docs/components/structured-data.tsx index 0a77701f859..cf9b74c6c09 100644 --- a/apps/docs/components/structured-data.tsx +++ b/apps/docs/components/structured-data.tsx @@ -70,10 +70,11 @@ export function StructuredData({ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Sim', - applicationCategory: 'DeveloperApplication', + applicationCategory: 'BusinessApplication', + applicationSubCategory: 'AI Workspace', operatingSystem: 'Any', description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.', url: baseUrl, author: { '@type': 'Organization', @@ -84,8 +85,9 @@ export function StructuredData({ category: 'Developer Tools', }, featureList: [ - 'AI agent creation', - 'Agentic workflow orchestration', + 'AI workspace for teams', + 'Mothership — natural language agent creation', + 'Visual workflow builder', '1,000+ integrations', 'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)', 'Knowledge base creation', diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx index 69fc92e8a12..0cd2aa9dbae 100644 --- a/apps/docs/content/docs/en/enterprise/index.mdx +++ b/apps/docs/content/docs/en/enterprise/index.mdx @@ -69,6 +69,9 @@ For self-hosted deployments, enterprise features can be enabled via environment | `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions | | `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC | | `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers | +| `INBOX_ENABLED`, `NEXT_PUBLIC_INBOX_ENABLED` | Sim Mailer inbox for outbound email | +| `WHITELABELING_ENABLED`, `NEXT_PUBLIC_WHITELABELING_ENABLED` | Custom branding and white-labeling | +| `AUDIT_LOGS_ENABLED`, `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` | Audit logging for compliance and monitoring | | `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Globally disable workspace/organization invitations | ### Organization Management diff --git a/apps/docs/content/docs/en/getting-started/index.mdx b/apps/docs/content/docs/en/getting-started/index.mdx index 0f25b32a621..f63be54314d 100644 --- a/apps/docs/content/docs/en/getting-started/index.mdx +++ b/apps/docs/content/docs/en/getting-started/index.mdx @@ -170,17 +170,17 @@ Build, test, and refine workflows quickly with immediate feedback ## Next Steps - - Discover API, Function, Condition, and other workflow blocks + + Discover API, Function, Condition, and other blocks - Connect 160+ services including Gmail, Slack, Notion, and more + Connect 1,000+ services including Gmail, Slack, Notion, and more Write custom functions for advanced data processing - - Make your workflow accessible via REST API or webhooks + + Make your agent accessible via REST API or webhooks @@ -188,7 +188,7 @@ Build, test, and refine workflows quickly with immediate feedback **Need detailed explanations?** Visit the [Blocks documentation](/blocks) for comprehensive guides on each component. -**Looking for integrations?** Explore the [Tools documentation](/tools) to see all 160+ available integrations. +**Looking for integrations?** Explore the [Tools documentation](/tools) to see all 1,000+ available integrations. **Ready to go live?** Learn about [Execution and Deployment](/execution) to make your workflows production-ready. @@ -199,5 +199,5 @@ Build, test, and refine workflows quickly with immediate feedback { question: "Can I use a different AI model instead of GPT-4o?", answer: "Yes. The Agent block supports models from OpenAI, Anthropic, Google, Groq, Cerebras, DeepSeek, Mistral, xAI, and more. You can select any available model from the dropdown. If you self-host, you can also use local models through Ollama." }, { question: "Can I import workflows from other tools?", answer: "Sim does not currently support importing workflows from other automation platforms. However, you can use the Copilot feature to describe what you want in natural language and have it build the workflow for you, which is often faster than manual recreation." }, { question: "What if my workflow does not produce the expected output?", answer: "Use the Chat panel to test iteratively and inspect outputs from each block. You can click the dropdown to view different block outputs and pinpoint where the issue is. The execution logs (accessible from the Logs tab) show detailed information about each step including token usage, costs, and any errors." }, - { question: "Where do I go after completing this tutorial?", answer: "Explore the Blocks documentation to learn about Condition, Router, Function, and API blocks. Browse the Tools section to discover 160+ integrations you can add to your agents. When you are ready to deploy, check the Execution docs for REST API, webhook, and scheduled trigger options." }, + { question: "Where do I go after completing this tutorial?", answer: "Explore the Blocks documentation to learn about Condition, Router, Function, and API blocks. Browse the Tools section to discover 1,000+ integrations you can add to your agents. When you are ready to deploy, check the Execution docs for REST API, webhook, and scheduled trigger options." }, ]} /> diff --git a/apps/docs/content/docs/en/index.mdx b/apps/docs/content/docs/en/index.mdx index a327eb44c08..6ad3ec93550 100644 --- a/apps/docs/content/docs/en/index.mdx +++ b/apps/docs/content/docs/en/index.mdx @@ -6,7 +6,7 @@ import { Card, Cards } from 'fumadocs-ui/components/card' # Sim Documentation -Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas. +Welcome to Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API — connected to 1,000+ integrations and every major LLM. ## Quick Start @@ -15,13 +15,13 @@ Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI Learn what you can build with Sim - Create your first workflow in 10 minutes + Build your first agent in 10 minutes - + Learn about the building blocks - Explore 80+ built-in integrations + Explore 1,000+ integrations @@ -35,10 +35,10 @@ Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI Work with workflow and environment variables - Monitor workflow runs and manage costs + Monitor agent runs and manage costs - Start workflows via API, webhooks, or schedules + Start agents via API, webhooks, or schedules diff --git a/apps/docs/content/docs/en/introduction/index.mdx b/apps/docs/content/docs/en/introduction/index.mdx index 1b6cdc9ecae..891667249f7 100644 --- a/apps/docs/content/docs/en/introduction/index.mdx +++ b/apps/docs/content/docs/en/introduction/index.mdx @@ -8,7 +8,7 @@ import { Image } from '@/components/ui/image' import { Video } from '@/components/ui/video' import { FAQ } from '@/components/ui/faq' -Sim is an open-source visual workflow builder for building and deploying AI agent workflows. Design intelligent automation systems using a no-code interface—connect AI models, databases, APIs, and business tools through an intuitive drag-and-drop canvas. Whether you're building chatbots, automating business processes, or orchestrating complex data pipelines, Sim provides the tools to bring your AI workflows to life. +Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. Connect AI models, databases, APIs, and 1,000+ business tools to build agents that automate real work — from chatbots and compliance agents to data pipelines and ITSM automation.
- Create your first workflow in 10 minutes + Build your first agent in 10 minutes - + Learn about the building blocks - Explore 160+ built-in integrations + Explore 1,000+ integrations Set up workspace roles and permissions @@ -121,9 +121,9 @@ Ready to build your first AI workflow? { question: "Is Sim free to use?", answer: "Sim offers a free Community plan with 1,000 one-time credits to get started. Paid plans start at $25/month (Pro) with 5,000 credits and go up to $100/month (Max) with 20,000 credits. Annual billing is available at a 15% discount. You can also self-host Sim for free on your own infrastructure." }, { question: "Is Sim open source?", answer: "Yes. Sim is open source under the Apache 2.0 license. The full source code is available on GitHub and you can self-host it, contribute to development, or modify it for your own needs. Enterprise features (SSO, access control) have a separate license that requires a subscription for production use." }, { question: "Which AI models and providers are supported?", answer: "Sim supports 15+ providers including OpenAI, Anthropic, Google Gemini, Groq, Cerebras, DeepSeek, Mistral, xAI, and OpenRouter. You can also run local models through Ollama or VLLM at no API cost. Bring Your Own Key (BYOK) is supported so you can use your own API keys at base provider pricing with no markup." }, - { question: "Do I need coding experience to use Sim?", answer: "No. Sim is a no-code visual builder where you design workflows by dragging blocks onto a canvas and connecting them. For advanced use cases, the Function block lets you write custom JavaScript, but it is entirely optional." }, + { question: "Do I need coding experience to use Sim?", answer: "No. Sim lets you build agents visually by dragging blocks onto a canvas and connecting them, or conversationally through Mothership using natural language. For advanced use cases, the Function block lets you write custom JavaScript, and the full API/SDK is available for programmatic access." }, { question: "Can I self-host Sim?", answer: "Yes. Sim provides Docker Compose configurations for self-hosted deployments. The stack includes the Sim application, a PostgreSQL database with pgvector, and a realtime collaboration server. You can also integrate local AI models via Ollama for a fully offline setup." }, { question: "Is there a limit on how many workflows I can create?", answer: "There is no limit on the number of workflows you can create on any plan. Usage limits apply to execution credits, rate limits, and file storage, which vary by plan tier." }, - { question: "What integrations are available?", answer: "Sim offers 160+ native integrations across categories including AI models, communication tools (Gmail, Slack, Teams, Telegram), productivity apps (Notion, Google Workspace, Airtable), development tools (GitHub, Jira, Linear), search services (Google Search, Perplexity, Exa), and databases (PostgreSQL, Supabase, Pinecone). For anything not built in, you can use the MCP (Model Context Protocol) support to connect custom services." }, - { question: "How does Sim compare to other workflow automation tools?", answer: "Sim is purpose-built for AI agent workflows rather than general task automation. It provides a visual canvas for orchestrating LLM-powered agents with built-in support for tool use, structured outputs, conditional branching, and real-time collaboration. The Copilot feature also lets you build and modify workflows using natural language." }, + { question: "What integrations are available?", answer: "Sim offers 1,000+ native integrations across categories including AI models, communication tools (Gmail, Slack, Teams, Telegram), productivity apps (Notion, Google Workspace, Airtable), development tools (GitHub, Jira, Linear), search services (Google Search, Perplexity, Exa), and databases (PostgreSQL, Supabase, Pinecone). For anything not built in, you can use the MCP (Model Context Protocol) support to connect custom services." }, + { question: "How does Sim compare to other AI agent builders?", answer: "Sim is an AI workspace — not just a workflow tool or an agent framework. It combines a visual workflow builder, Mothership for natural-language agent creation, knowledge bases, tables, and full observability in one environment. Teams build agents visually, conversationally, or with code, then deploy and manage them with enterprise governance, real-time collaboration, and staging-to-production workflows." }, ]} /> diff --git a/apps/docs/public/favicon/site.webmanifest b/apps/docs/public/favicon/site.webmanifest index 9bb874a0218..8905a552b58 100644 --- a/apps/docs/public/favicon/site.webmanifest +++ b/apps/docs/public/favicon/site.webmanifest @@ -1,7 +1,7 @@ { - "name": "Sim Documentation — Build AI Agents & Run Your Agentic Workforce", + "name": "Sim Documentation — The AI Workspace for Teams", "short_name": "Sim Docs", - "description": "Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.", + "description": "Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.", "start_url": "/", "scope": "/", "icons": [ diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt index 8d03f2491c3..aad99e364ef 100644 --- a/apps/docs/public/llms.txt +++ b/apps/docs/public/llms.txt @@ -1,13 +1,15 @@ # Sim Documentation -Sim is a visual workflow builder for AI applications that lets you build AI agent workflows visually. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required. +Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API — connected to 1,000+ integrations and every major LLM. ## What is Sim? -Sim provides a complete ecosystem for AI workflow automation including: +Sim provides a complete AI workspace including: +- Mothership — natural language agent creation and workspace management - Visual workflow builder with drag-and-drop interface -- AI agent creation and automation -- 80+ built-in integrations (OpenAI, Slack, Gmail, GitHub, etc.) +- 1,000+ built-in integrations (OpenAI, Anthropic, Slack, Gmail, GitHub, etc.) +- Knowledge bases for retrieval-augmented generation +- Built-in tables for structured data - Real-time team collaboration - Multiple deployment options (cloud-hosted or self-hosted) - Custom integrations via MCP protocol @@ -16,22 +18,22 @@ Sim provides a complete ecosystem for AI workflow automation including: Here are the key areas covered in our documentation: -/introduction - Getting started with Sim visual workflow builder -/getting-started - Quick start guide for building your first workflow -/blocks - Understanding workflow blocks (AI agents, APIs, functions) -/tools - 80+ built-in integrations and tools +/introduction - Getting started with Sim AI workspace +/getting-started - Quick start guide for building your first agent +/blocks - Understanding blocks (AI agents, APIs, functions) +/tools - 1,000+ integrations and tools /webhooks - Webhook triggers and handling /mcp - Custom integrations via MCP protocol /deployment - Cloud-hosted vs self-hosted deployment /permissions - Team collaboration and workspace management /collaboration - Real-time editing and team features -/workflows - Building complex automation workflows +/workflows - Building agent logic with the visual builder ## Technical Information - Framework: Fumadocs (Next.js-based documentation platform) - Content: MDX files with interactive examples -- Languages: English (primary), French, Chinese +- Languages: English (primary), Spanish, French, German, Japanese, Chinese - Search: AI-powered search and assistance available ## Complete Documentation @@ -40,14 +42,10 @@ For the full documentation with all pages, examples, and interactive features, v ## Additional Resources -- GitHub repository with workflow examples +- GitHub repository with agent examples - Discord community for support and discussions -- 80+ built-in integrations with detailed guides +- 1,000+ built-in integrations with detailed guides - MCP protocol documentation for custom integrations - Self-hosting guides and Docker deployment -For the complete documentation with interactive examples and visual workflow builder guides, visit https://docs.sim.ai - ---- - -Last updated: 2025-09-15 \ No newline at end of file +For the complete documentation visit https://docs.sim.ai diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 8a43548acb4..27119bf1806 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -1,6 +1,6 @@ 'use client' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { Eye, EyeOff, Loader2 } from 'lucide-react' import Link from 'next/link' @@ -20,6 +20,7 @@ import { validateCallbackUrl } from '@/lib/core/security/input-validation' import { cn } from '@/lib/core/utils/cn' import { getBaseUrl } from '@/lib/core/utils/urls' import { quickValidateEmail } from '@/lib/messaging/email/validation' +import { captureClientEvent } from '@/lib/posthog/client' import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' @@ -113,6 +114,10 @@ export default function LoginPage({ : null ) + useEffect(() => { + captureClientEvent('login_page_viewed', {}) + }, []) + const handleEmailChange = (e: React.ChangeEvent) => { const newEmail = e.target.value setEmail(newEmail) diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index afb27cd729a..5d1b2d25ff6 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -12,7 +12,7 @@ import { client, useSession } from '@/lib/auth/auth-client' import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' import { quickValidateEmail } from '@/lib/messaging/email/validation' -import { captureEvent } from '@/lib/posthog/client' +import { captureClientEvent, captureEvent } from '@/lib/posthog/client' import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' @@ -71,15 +71,13 @@ const validateEmailField = (emailValue: string): string[] => { return errors } -function SignupFormContent({ - githubAvailable, - googleAvailable, - isProduction, -}: { +interface SignupFormProps { githubAvailable: boolean googleAvailable: boolean isProduction: boolean -}) { +} + +function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: SignupFormProps) { const router = useRouter() const searchParams = useSearchParams() const { refetch: refetchSession } = useSession() @@ -87,8 +85,8 @@ function SignupFormContent({ const [isLoading, setIsLoading] = useState(false) useEffect(() => { - captureEvent(posthog, 'signup_page_viewed', {}) - }, [posthog]) + captureClientEvent('signup_page_viewed', {}) + }, []) const [showPassword, setShowPassword] = useState(false) const [password, setPassword] = useState('') const [passwordErrors, setPasswordErrors] = useState([]) @@ -243,8 +241,6 @@ function SignupFormContent({ return } - const sanitizedName = trimmedName - let token: string | undefined const widget = turnstileRef.current if (turnstileSiteKey && widget) { @@ -267,7 +263,7 @@ function SignupFormContent({ { email: emailValue, password: passwordValue, - name: sanitizedName, + name: trimmedName, }, { headers: { @@ -629,11 +625,7 @@ export default function SignupPage({ githubAvailable, googleAvailable, isProduction, -}: { - githubAvailable: boolean - googleAvailable: boolean - isProduction: boolean -}) { +}: SignupFormProps) { return ( Loading...
} diff --git a/apps/sim/app/(landing)/blog/layout.tsx b/apps/sim/app/(landing)/blog/layout.tsx index 6d505132e37..512f41a32ee 100644 --- a/apps/sim/app/(landing)/blog/layout.tsx +++ b/apps/sim/app/(landing)/blog/layout.tsx @@ -10,7 +10,7 @@ export default async function StudioLayout({ children }: { children: React.React name: 'Sim', url: 'https://sim.ai', description: - 'Sim is an open-source platform for building, testing, and deploying AI agent workflows.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.', logo: 'https://sim.ai/logo/primary/small.png', sameAs: [ 'https://x.com/simdotai', diff --git a/apps/sim/app/(landing)/blog/page.tsx b/apps/sim/app/(landing)/blog/page.tsx index 9bae1dd8aea..a7339cc76ad 100644 --- a/apps/sim/app/(landing)/blog/page.tsx +++ b/apps/sim/app/(landing)/blog/page.tsx @@ -19,8 +19,8 @@ export async function generateMetadata({ const title = titleParts.join(' — ') const description = tag - ? `Sim blog posts tagged "${tag}" — insights and guides for building AI agent workflows.` - : 'Announcements, insights, and guides for building AI agent workflows.' + ? `Sim blog posts tagged "${tag}" — insights and guides for building AI agents.` + : 'Announcements, insights, and guides for building AI agents.' const canonicalParams = new URLSearchParams() if (tag) canonicalParams.set('tag', tag) diff --git a/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx new file mode 100644 index 00000000000..d7a213f2499 --- /dev/null +++ b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx @@ -0,0 +1,250 @@ +'use client' + +import { useCallback, useEffect, useMemo, useState } from 'react' +import { createLogger } from '@sim/logger' +import { Loader2, X } from 'lucide-react' +import Image from 'next/image' +import { useRouter } from 'next/navigation' +import { Modal, ModalClose, ModalContent, ModalTitle, ModalTrigger } from '@/components/emcn' +import { GithubIcon, GoogleIcon } from '@/components/icons' +import { client } from '@/lib/auth/auth-client' +import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' +import { captureClientEvent } from '@/lib/posthog/client' +import type { PostHogEventMap } from '@/lib/posthog/events' +import { getBrandConfig } from '@/ee/whitelabeling' + +const logger = createLogger('AuthModal') + +type AuthView = 'login' | 'signup' + +interface AuthModalProps { + children: React.ReactNode + defaultView?: AuthView + source: PostHogEventMap['auth_modal_opened']['source'] +} + +interface ProviderStatus { + githubAvailable: boolean + googleAvailable: boolean + registrationDisabled: boolean +} + +let fetchPromise: Promise | null = null + +const FALLBACK_STATUS: ProviderStatus = { + githubAvailable: false, + googleAvailable: false, + registrationDisabled: false, +} + +const SOCIAL_BTN = + 'relative flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--landing-border-strong)] text-[13.5px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)] disabled:cursor-not-allowed disabled:opacity-50' + +function fetchProviderStatus(): Promise { + if (fetchPromise) return fetchPromise + fetchPromise = fetch('/api/auth/providers') + .then((r) => { + if (!r.ok) throw new Error(`HTTP ${r.status}`) + return r.json() + }) + .then(({ githubAvailable, googleAvailable, registrationDisabled }: ProviderStatus) => ({ + githubAvailable, + googleAvailable, + registrationDisabled, + })) + .catch(() => { + fetchPromise = null + return FALLBACK_STATUS + }) + return fetchPromise +} + +export function AuthModal({ children, defaultView = 'login', source }: AuthModalProps) { + const router = useRouter() + const [open, setOpen] = useState(false) + const [view, setView] = useState(defaultView) + const [providerStatus, setProviderStatus] = useState(null) + const [socialLoading, setSocialLoading] = useState<'github' | 'google' | null>(null) + const brand = useMemo(() => getBrandConfig(), []) + + useEffect(() => { + fetchProviderStatus().then(setProviderStatus) + }, []) + + const hasSocial = providerStatus?.githubAvailable || providerStatus?.googleAvailable + const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) + const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) + const hasModalContent = hasSocial || ssoEnabled + + useEffect(() => { + if (!open || !providerStatus) return + if (!hasModalContent) { + setOpen(false) + router.push(defaultView === 'login' ? '/login' : '/signup') + return + } + if (providerStatus.registrationDisabled && view === 'signup') { + setView('login') + } + }, [open, providerStatus, hasModalContent, defaultView, router, view]) + + const handleOpenChange = useCallback( + (nextOpen: boolean) => { + if (nextOpen && providerStatus && !hasModalContent) { + router.push(defaultView === 'login' ? '/login' : '/signup') + return + } + setOpen(nextOpen) + if (nextOpen) { + const initialView = + defaultView === 'signup' && providerStatus?.registrationDisabled ? 'login' : defaultView + setView(initialView) + captureClientEvent('auth_modal_opened', { view: initialView, source }) + } + }, + [defaultView, hasModalContent, providerStatus, router, source] + ) + + const handleSocialLogin = useCallback(async (provider: 'github' | 'google') => { + setSocialLoading(provider) + try { + await client.signIn.social({ provider, callbackURL: '/workspace' }) + } catch (error) { + logger.warn('Social sign-in did not complete', { provider, error }) + } finally { + setSocialLoading(null) + } + }, []) + + const handleSSOLogin = useCallback(() => { + setOpen(false) + router.push('/sso') + }, [router]) + + const handleEmailContinue = useCallback(() => { + setOpen(false) + router.push(view === 'login' ? '/login' : '/signup') + }, [router, view]) + + return ( + + {children} + + + {view === 'login' ? 'Log in' : 'Create account'} + + +
+ + + Close + + + {!providerStatus ? ( +
+ +
+ ) : ( + <> +
+ {brand.name} +
+

+ Start building. +

+

+ {view === 'login' ? 'Log in to continue' : 'Create free account'} +

+
+
+ +
+ {providerStatus.googleAvailable && ( + + )} + {providerStatus.githubAvailable && ( + + )} + {ssoEnabled && ( + + )} +
+ + {emailEnabled && ( + <> +
+
+
+
+
+ + Or + +
+
+ + + + )} + +
+ + {view === 'login' ? "Don't have an account? " : 'Already have an account? '} + + {view === 'login' && providerStatus.registrationDisabled ? ( + Registration is disabled + ) : ( + + )} +
+ + )} +
+ + + ) +} diff --git a/apps/sim/app/(landing)/components/collaboration/collaboration.tsx b/apps/sim/app/(landing)/components/collaboration/collaboration.tsx index 5db2aad66c0..13200885ba7 100644 --- a/apps/sim/app/(landing)/components/collaboration/collaboration.tsx +++ b/apps/sim/app/(landing)/components/collaboration/collaboration.tsx @@ -4,6 +4,8 @@ import { useCallback, useEffect, useRef, useState } from 'react' import Image from 'next/image' import Link from 'next/link' import { Badge } from '@/components/emcn' +import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal' +import { trackLandingCta } from '@/app/(landing)/landing-analytics' interface DotGridProps { className?: string @@ -250,10 +252,10 @@ export default function Collaboration() {

- Sim supports real-time multiplayer collaboration. Teams can build AI agents together - in a shared workspace with live cursors, presence indicators, and concurrent editing. - Features include role-based access control, shared workflows, and team workspace - management. + Sim supports real-time multiplayer collaboration. Teams build AI agents together in a + shared workspace with live cursors, presence indicators, and concurrent editing. + Features include role-based access control, shared agents and workflows, and team + workspace management.

@@ -261,45 +263,54 @@ export default function Collaboration() { in real-time inside your workspace.

- - Build together - + - + xmlns='http://www.w3.org/2000/svg' + > + + + + +
Collaboration visual showing team workflows with real-time editing, shared cursors, and version control interface ))} - Design powerful workflows, connect your data, and monitor every run — all in one - platform. + Build agents, connect your data, and monitor every run — all in one workspace.
@@ -265,12 +265,21 @@ export default function Features() { {FEATURE_TABS[activeTab].description}

- - {FEATURE_TABS[activeTab].cta} - + + + diff --git a/apps/sim/app/(landing)/components/footer/footer-cta.tsx b/apps/sim/app/(landing)/components/footer/footer-cta.tsx index f9af4ac4bcc..9a06d79fafe 100644 --- a/apps/sim/app/(landing)/components/footer/footer-cta.tsx +++ b/apps/sim/app/(landing)/components/footer/footer-cta.tsx @@ -2,8 +2,9 @@ import { useCallback, useRef, useState } from 'react' import { ArrowUp } from 'lucide-react' -import Link from 'next/link' +import { cn } from '@/lib/core/utils/cn' import { captureClientEvent } from '@/lib/posthog/client' +import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal' import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel' import { trackLandingCta } from '@/app/(landing)/landing-analytics' import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder' @@ -70,8 +71,8 @@ export function FooterCTA() { aria-label='Describe what you want to build' placeholder={animatedPlaceholder} rows={2} - className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text)] text-base leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0' - style={{ caretColor: '#FFFFFF', maxHeight: `${MAX_HEIGHT}px` }} + className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text)] text-base leading-[24px] tracking-[-0.015em] caret-white outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0' + style={{ maxHeight: `${MAX_HEIGHT}px` }} />
@@ -96,7 +96,10 @@ export function FooterCTA() { href='https://docs.sim.ai' target='_blank' rel='noopener noreferrer' - className={`${CTA_BUTTON} border-[var(--landing-border-strong)] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]`} + className={cn( + CTA_BUTTON, + 'border-[var(--landing-border-strong)] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]' + )} onClick={() => trackLandingCta({ label: 'Docs', @@ -107,15 +110,24 @@ export function FooterCTA() { > Docs - - trackLandingCta({ label: 'Get started', section: 'footer_cta', destination: '/signup' }) - } - > - Get started - + + +
) diff --git a/apps/sim/app/(landing)/components/footer/footer.tsx b/apps/sim/app/(landing)/components/footer/footer.tsx index 6cd9c3f7f3e..a27a1cf30bf 100644 --- a/apps/sim/app/(landing)/components/footer/footer.tsx +++ b/apps/sim/app/(landing)/components/footer/footer.tsx @@ -1,5 +1,6 @@ import Image from 'next/image' import Link from 'next/link' +import { cn } from '@/lib/core/utils/cn' import { FooterCTA } from '@/app/(landing)/components/footer/footer-cta' const LINK_CLASS = @@ -9,25 +10,24 @@ interface FooterItem { label: string href: string external?: boolean - arrow?: boolean externalArrow?: boolean } const PRODUCT_LINKS: FooterItem[] = [ - { label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true }, - { label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true }, + { label: 'Mothership', href: 'https://docs.sim.ai', external: true }, + { label: 'Workflows', href: 'https://docs.sim.ai', external: true }, { label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true }, { label: 'Tables', href: 'https://docs.sim.ai/tables', external: true }, + { label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true }, { label: 'API', href: 'https://docs.sim.ai/api-reference/getting-started', external: true }, + { label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true }, { label: 'Status', href: 'https://status.sim.ai', external: true, externalArrow: true }, ] const RESOURCES_LINKS: FooterItem[] = [ { label: 'Blog', href: '/blog' }, - // { label: 'Templates', href: '/templates' }, { label: 'Docs', href: 'https://docs.sim.ai', external: true }, { label: 'Models', href: '/models' }, - // { label: 'Academy', href: '/academy' }, { label: 'Partners', href: '/partners' }, { label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true, externalArrow: true }, { label: 'Changelog', href: '/changelog' }, @@ -47,7 +47,7 @@ const BLOCK_LINKS: FooterItem[] = [ ] const INTEGRATION_LINKS: FooterItem[] = [ - { label: 'All Integrations', href: '/integrations', arrow: true }, + { label: 'All Integrations', href: '/integrations' }, { label: 'Confluence', href: 'https://docs.sim.ai/tools/confluence', external: true }, { label: 'Slack', href: 'https://docs.sim.ai/tools/slack', external: true }, { label: 'GitHub', href: 'https://docs.sim.ai/tools/github', external: true }, @@ -93,7 +93,7 @@ const LEGAL_LINKS: FooterItem[] = [ function ChevronArrow({ external }: { external?: boolean }) { return (

{title}

- {items.map(({ label, href, external, arrow, externalArrow }) => + {items.map(({ label, href, external, externalArrow }) => external ? ( {label} {externalArrow && } ) : ( - + {label} - {arrow && } ) )} @@ -162,7 +160,10 @@ export default function Footer({ hideCTA }: FooterProps) { return (
{!hideCTA && }
diff --git a/apps/sim/app/(landing)/components/hero/hero.tsx b/apps/sim/app/(landing)/components/hero/hero.tsx index 7098c4abf71..217dd210edf 100644 --- a/apps/sim/app/(landing)/components/hero/hero.tsx +++ b/apps/sim/app/(landing)/components/hero/hero.tsx @@ -1,7 +1,8 @@ 'use client' import dynamic from 'next/dynamic' -import Link from 'next/link' +import { cn } from '@/lib/core/utils/cn' +import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal' import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal' import { trackLandingCta } from '@/app/(landing)/landing-analytics' @@ -16,7 +17,6 @@ const LandingPreview = dynamic( } ) -/** Shared base classes for CTA link buttons — matches Deploy/Run button styling in the preview panel. */ const CTA_BASE = 'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm' @@ -30,11 +30,11 @@ export default function Hero() { className='relative flex flex-col items-center overflow-hidden bg-[var(--landing-bg)] pt-[60px] lg:pt-[100px]' >

- Sim is an open-source AI agent platform. Sim lets teams build AI agents and run an agentic - workforce by connecting 1,000+ integrations and LLMs — including OpenAI, Anthropic Claude, - Google Gemini, Mistral, and xAI Grok — to deploy and orchestrate agentic workflows. Users - create agents, workflows, knowledge bases, tables, and docs. Sim is trusted by over 100,000 - builders at startups and Fortune 500 companies. Sim is SOC2 compliant. + Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect + 1,000+ integrations and every major LLM — including OpenAI, Anthropic Claude, Google Gemini, + Mistral, and xAI Grok — to create agents that automate real work. Build agents visually with + the workflow builder, conversationally through Mothership, or programmatically with the API. + Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 compliant.

@@ -56,7 +56,10 @@ export default function Hero() { - - trackLandingCta({ label: 'Get started', section: 'hero', destination: '/signup' }) - } - > - Get started - + + +
diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx index ef5929963e7..6e7ba497aeb 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx @@ -3,13 +3,13 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react' import { AnimatePresence, motion } from 'framer-motion' import { ArrowUp } from 'lucide-react' -import Link from 'next/link' import { useRouter } from 'next/navigation' import { createPortal } from 'react-dom' import { Blimp, BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn' import { AgentIcon, HubspotIcon, OpenAIIcon, SalesforceIcon } from '@/components/icons' import { LandingPromptStorage } from '@/lib/core/utils/browser-storage' import { captureClientEvent } from '@/lib/posthog/client' +import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal' import { EASE_OUT, type EditorPromptData, @@ -19,6 +19,7 @@ import { TYPE_INTERVAL_MS, TYPE_START_BUFFER_MS, } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data' +import { trackLandingCta } from '@/app/(landing)/landing-analytics' type PanelTab = 'copilot' | 'editor' @@ -44,6 +45,11 @@ export function useLandingSubmit() { const trimmed = text.trim() if (!trimmed) return LandingPromptStorage.store(trimmed) + trackLandingCta({ + label: 'Prompt submit', + section: 'landing_preview', + destination: '/signup', + }) router.push('/signup') }, [router] @@ -175,20 +181,29 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel({
- setCursorPos({ x: e.clientX, y: e.clientY })} - onMouseLeave={() => setCursorPos(null)} - > -
- Deploy -
-
- - Run -
- + + + {cursorPos && createPortal(
- Platform + Workspace
- {PLATFORM.map((link) => ( + {WORKSPACE.map((link) => ( ))}
diff --git a/apps/sim/app/(landing)/components/navbar/navbar.tsx b/apps/sim/app/(landing)/components/navbar/navbar.tsx index 8f595d69078..7e8d7342616 100644 --- a/apps/sim/app/(landing)/components/navbar/navbar.tsx +++ b/apps/sim/app/(landing)/components/navbar/navbar.tsx @@ -7,6 +7,7 @@ import { useSearchParams } from 'next/navigation' import { GithubOutlineIcon } from '@/components/icons' import { useSession } from '@/lib/auth/auth-client' import { cn } from '@/lib/core/utils/cn' +import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal' import { BlogDropdown, type NavBlogPost, @@ -29,6 +30,8 @@ interface NavLink { const NAV_LINKS: NavLink[] = [ { label: 'Docs', href: 'https://docs.sim.ai', external: true, icon: 'chevron', dropdown: 'docs' }, { label: 'Blog', href: '/blog', icon: 'chevron', dropdown: 'blog' }, + { label: 'Integrations', href: '/integrations' }, + { label: 'Models', href: '/models' }, { label: 'Pricing', href: '/#pricing' }, ] @@ -225,30 +228,38 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps ) : ( <> - - trackLandingCta({ label: 'Log in', section: 'navbar', destination: '/login' }) - } - > - Log in - - - trackLandingCta({ - label: 'Get started', - section: 'navbar', - destination: '/signup', - }) - } - > - Get started - + + + + + + )}
@@ -335,32 +346,38 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps ) : ( <> - { - trackLandingCta({ label: 'Log in', section: 'navbar', destination: '/login' }) - setMobileMenuOpen(false) - }} - aria-label='Log in' - > - Log in - - { - trackLandingCta({ - label: 'Get started', - section: 'navbar', - destination: '/signup', - }) - setMobileMenuOpen(false) - }} - aria-label='Get started with Sim' - > - Get started - + + + + + + )}
diff --git a/apps/sim/app/(landing)/components/pricing/pricing.tsx b/apps/sim/app/(landing)/components/pricing/pricing.tsx index d4d0789467c..dac481c5908 100644 --- a/apps/sim/app/(landing)/components/pricing/pricing.tsx +++ b/apps/sim/app/(landing)/components/pricing/pricing.tsx @@ -1,7 +1,7 @@ 'use client' -import Link from 'next/link' import { Badge } from '@/components/emcn' +import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal' import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal' import { trackLandingCta } from '@/app/(landing)/landing-analytics' @@ -37,7 +37,7 @@ const PRICING_TIERS: PricingTier[] = [ { id: 'pro', name: 'Pro', - description: 'For professionals building production workflows', + description: 'For professionals deploying AI agents', price: '$25', billingPeriod: 'per month', color: '#00F701', @@ -55,7 +55,7 @@ const PRICING_TIERS: PricingTier[] = [ { id: 'max', name: 'Max', - description: 'For power users and teams building at scale', + description: 'For teams building AI agents at scale', price: '$100', billingPeriod: 'per month', color: '#FA4EDF', @@ -163,33 +163,37 @@ function PricingCard({ tier }: PricingCardProps) { ) : isPro ? ( - - trackLandingCta({ - label: tier.cta.label, - section: 'pricing', - destination: tier.cta.href || '/signup', - }) - } - > - {tier.cta.label} - + + + ) : ( - - trackLandingCta({ - label: tier.cta.label, - section: 'pricing', - destination: tier.cta.href || '/signup', - }) - } - > - {tier.cta.label} - + + + )} diff --git a/apps/sim/app/(landing)/components/structured-data.tsx b/apps/sim/app/(landing)/components/structured-data.tsx index 1fc0122650a..b03c4fb45e9 100644 --- a/apps/sim/app/(landing)/components/structured-data.tsx +++ b/apps/sim/app/(landing)/components/structured-data.tsx @@ -27,7 +27,7 @@ export default function StructuredData() { name: 'Sim', alternateName: 'Sim Studio', description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.', url: 'https://sim.ai', logo: { '@type': 'ImageObject', @@ -55,9 +55,9 @@ export default function StructuredData() { '@type': 'WebSite', '@id': 'https://sim.ai/#website', url: 'https://sim.ai', - name: 'Sim — Build AI Agents & Run Your Agentic Workforce', + name: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents', description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Join 100,000+ builders.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM. Join 100,000+ builders.', publisher: { '@id': 'https://sim.ai/#organization' }, inLanguage: 'en-US', }, @@ -65,13 +65,13 @@ export default function StructuredData() { '@type': 'WebPage', '@id': 'https://sim.ai/#webpage', url: 'https://sim.ai', - name: 'Sim — Build AI Agents & Run Your Agentic Workforce', + name: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents', isPartOf: { '@id': 'https://sim.ai/#website' }, about: { '@id': 'https://sim.ai/#software' }, datePublished: '2024-01-01T00:00:00+00:00', dateModified: new Date().toISOString(), description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.', breadcrumb: { '@id': 'https://sim.ai/#breadcrumb' }, inLanguage: 'en-US', speakable: { @@ -91,12 +91,14 @@ export default function StructuredData() { '@type': 'WebApplication', '@id': 'https://sim.ai/#software', url: 'https://sim.ai', - name: 'Sim — Build AI Agents & Run Your Agentic Workforce', + name: 'Sim — The AI Workspace', description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 compliant.', - applicationCategory: 'DeveloperApplication', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders. SOC2 compliant.', + applicationCategory: 'BusinessApplication', + applicationSubCategory: 'AI Workspace', operatingSystem: 'Web', browserRequirements: 'Requires a modern browser with JavaScript enabled', + installUrl: 'https://sim.ai/signup', offers: [ { '@type': 'Offer', @@ -135,8 +137,9 @@ export default function StructuredData() { }, ], featureList: [ - 'AI agent creation', - 'Agentic workflow orchestration', + 'AI workspace for teams', + 'Mothership — natural language agent creation', + 'Visual workflow builder', '1,000+ integrations', 'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)', 'Knowledge base creation', @@ -176,19 +179,27 @@ export default function StructuredData() { codeRepository: 'https://github.com/simstudioai/sim', programmingLanguage: ['TypeScript', 'Python'], runtimePlatform: 'Node.js', - license: 'https://opensource.org/licenses/AGPL-3.0', + license: 'https://opensource.org/licenses/Apache-2.0', isPartOf: { '@id': 'https://sim.ai/#software' }, }, { '@type': 'FAQPage', '@id': 'https://sim.ai/#faq', mainEntity: [ + { + '@type': 'Question', + name: 'What is the best AI agent builder?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Sim is the open-source AI workspace trusted by over 100,000 builders for creating, deploying, and managing AI agents. Build agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. Sim connects to 1,000+ integrations and all major LLMs (OpenAI, Anthropic, Google, xAI, Mistral), and includes knowledge bases, tables, real-time collaboration, and enterprise governance. Free tier available. SOC2 compliant. Self-hostable.', + }, + }, { '@type': 'Question', name: 'What is Sim?', acceptedAnswer: { '@type': 'Answer', - text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 compliant.', + text: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. The workspace includes Mothership for natural-language creation, a visual workflow builder, knowledge bases, tables, and full observability. Trusted by over 100,000 builders. SOC2 compliant.', }, }, { @@ -212,7 +223,7 @@ export default function StructuredData() { name: 'Do I need coding skills to use Sim?', acceptedAnswer: { '@type': 'Answer', - text: 'No coding skills are required. Sim provides a visual interface for building AI agents and agentic workflows. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.', + text: 'No coding skills are required. Sim provides multiple ways to build agents: a visual workflow builder for drag-and-drop creation, Mothership for building in natural language, and templates for common use cases. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.', }, }, { @@ -228,7 +239,7 @@ export default function StructuredData() { name: 'Is Sim open source?', acceptedAnswer: { '@type': 'Answer', - text: 'Yes. Sim is fully open source under the AGPL-3.0 license. The source code is available on GitHub at github.com/simstudioai/sim. You can self-host Sim or use the hosted version at sim.ai.', + text: 'Yes. Sim is fully open source under the Apache 2.0 license. The source code is available on GitHub at github.com/simstudioai/sim. You can self-host Sim or use the hosted version at sim.ai.', }, }, { diff --git a/apps/sim/app/(landing)/components/templates/templates.tsx b/apps/sim/app/(landing)/components/templates/templates.tsx index ecb9ae7fd37..cb799237ae3 100644 --- a/apps/sim/app/(landing)/components/templates/templates.tsx +++ b/apps/sim/app/(landing)/components/templates/templates.tsx @@ -9,6 +9,7 @@ import { Badge, ChevronDown } from '@/components/emcn' import { LandingWorkflowSeedStorage } from '@/lib/core/utils/browser-storage' import { cn } from '@/lib/core/utils/cn' import { TEMPLATE_WORKFLOWS } from '@/app/(landing)/components/templates/template-workflows' +import { trackLandingCta } from '@/app/(landing)/landing-analytics' const logger = createLogger('LandingTemplates') @@ -297,6 +298,11 @@ export default function Templates() { }) } finally { setIsPreparingTemplate(false) + trackLandingCta({ + label: activeWorkflow.name, + section: 'templates', + destination: '/signup', + }) router.push('/signup') } }, [ diff --git a/apps/sim/app/(landing)/integrations/[slug]/components/integration-cta-button.tsx b/apps/sim/app/(landing)/integrations/[slug]/components/integration-cta-button.tsx new file mode 100644 index 00000000000..b746d7a5277 --- /dev/null +++ b/apps/sim/app/(landing)/integrations/[slug]/components/integration-cta-button.tsx @@ -0,0 +1,26 @@ +'use client' + +import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal' +import { trackLandingCta } from '@/app/(landing)/landing-analytics' + +interface IntegrationCtaButtonProps { + children: React.ReactNode + className?: string + label: string +} + +export function IntegrationCtaButton({ children, className, label }: IntegrationCtaButtonProps) { + return ( + + + + ) +} diff --git a/apps/sim/app/(landing)/integrations/[slug]/components/template-card-button.tsx b/apps/sim/app/(landing)/integrations/[slug]/components/template-card-button.tsx index 5fffa1121b6..4f2a91d7e73 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/components/template-card-button.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/components/template-card-button.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation' import { LandingPromptStorage } from '@/lib/core/utils/browser-storage' import { cn } from '@/lib/core/utils/cn' +import { trackLandingCta } from '@/app/(landing)/landing-analytics' interface TemplateCardButtonProps { prompt: string @@ -15,6 +16,7 @@ export function TemplateCardButton({ prompt, className, children }: TemplateCard function handleClick() { LandingPromptStorage.store(prompt) + trackLandingCta({ label: 'Template card', section: 'integrations', destination: '/signup' }) router.push('/signup') } diff --git a/apps/sim/app/(landing)/integrations/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/[slug]/page.tsx index 35290f9e711..e93bf9c73fe 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/page.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/page.tsx @@ -3,13 +3,14 @@ import Image from 'next/image' import Link from 'next/link' import { notFound } from 'next/navigation' import { getBaseUrl } from '@/lib/core/utils/urls' +import { IntegrationCtaButton } from '@/app/(landing)/integrations/[slug]/components/integration-cta-button' +import { IntegrationFAQ } from '@/app/(landing)/integrations/[slug]/components/integration-faq' +import { TemplateCardButton } from '@/app/(landing)/integrations/[slug]/components/template-card-button' +import { IntegrationIcon } from '@/app/(landing)/integrations/components/integration-icon' +import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mapping' +import integrations from '@/app/(landing)/integrations/data/integrations.json' +import type { AuthType, FAQItem, Integration } from '@/app/(landing)/integrations/data/types' import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/template-prompts/consts' -import { IntegrationIcon } from '../components/integration-icon' -import { blockTypeToIconMap } from '../data/icon-mapping' -import integrations from '../data/integrations.json' -import type { AuthType, FAQItem, Integration } from '../data/types' -import { IntegrationFAQ } from './components/integration-faq' -import { TemplateCardButton } from './components/template-card-button' const allIntegrations = integrations as Integration[] const INTEGRATION_COUNT = allIntegrations.length @@ -85,18 +86,18 @@ function buildFAQs(integration: Integration): FAQItem[] { const faqs: FAQItem[] = [ { question: `What is Sim's ${name} integration?`, - answer: `Sim's ${name} integration lets you build AI-powered workflows that automate tasks in ${name} without writing code. ${description} You can connect ${name} to hundreds of other services in the same workflow — from CRMs and spreadsheets to messaging tools and databases.`, + answer: `Sim's ${name} integration lets you build AI agents that automate tasks in ${name} without writing code. ${description} You can connect ${name} to hundreds of other services in the same agent — from CRMs and spreadsheets to messaging tools and databases.`, }, { question: `What can I automate with ${name} in Sim?`, answer: topOpNames.length > 0 ? `With Sim you can: ${topOpNames.join('; ')}${operations.length > 5 ? `; and ${operations.length - 5} more tools` : ''}. Each action runs inside an AI agent block, so you can combine ${name} with LLM reasoning, conditional logic, and data from any other connected service.` - : `Sim lets you automate ${name} workflows by connecting it to an AI agent that can read from it, write to it, and chain it together with other services — all driven by natural-language instructions instead of rigid rules.`, + : `Sim lets you automate ${name} by connecting it to an AI agent that can read from it, write to it, and chain it together with other services — all driven by natural-language instructions instead of rigid rules.`, }, { question: `How do I connect ${name} to Sim?`, - answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open a new workflow. (3) Drag a ${name} block onto the canvas. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your automation is live.`, + answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open your workspace and create an agent. (3) Drag a ${name} block onto the workflow builder. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your agent is live.`, }, { question: `Can I use ${name} as a tool inside an AI agent in Sim?`, @@ -106,19 +107,19 @@ function buildFAQs(integration: Integration): FAQItem[] { ? [ { question: `How do I ${topOpNames[0].toLowerCase()} with ${name} in Sim?`, - answer: `Add a ${name} block to your workflow and select "${topOpNames[0]}" as the tool. Fill in the required fields — you can reference outputs from earlier steps, such as text generated by an AI agent or data fetched from another integration. No code is required.`, + answer: `Add a ${name} block to your agent and select "${topOpNames[0]}" as the tool. Fill in the required fields — you can reference outputs from earlier steps, such as text generated by an AI agent or data fetched from another integration. No code is required.`, }, ] : []), ...(triggers.length > 0 ? [ { - question: `How do I trigger a Sim workflow from ${name} automatically?`, - answer: `Add a ${name} trigger block to your workflow and copy the generated webhook URL. Paste that URL into ${name}'s webhook settings and select the events you want to listen for (${triggers.map((t) => t.name).join(', ')}). From that point on, every matching event in ${name} instantly fires your workflow — no polling, no delay.`, + question: `How do I trigger a Sim agent from ${name} automatically?`, + answer: `Add a ${name} trigger block to your agent and copy the generated webhook URL. Paste that URL into ${name}'s webhook settings and select the events you want to listen for (${triggers.map((t) => t.name).join(', ')}). From that point on, every matching event in ${name} instantly runs your agent — no polling, no delay.`, }, { - question: `What data does Sim receive when a ${name} event triggers a workflow?`, - answer: `When ${name} fires a webhook, Sim receives the full event payload that ${name} sends — typically the record or object that changed, along with metadata like the event type and timestamp. Inside your workflow, every field from that payload is available as a variable you can pass to AI agents, conditions, or other integrations.`, + question: `What data does Sim receive when a ${name} event triggers an agent?`, + answer: `When ${name} fires a webhook, Sim receives the full event payload that ${name} sends — typically the record or object that changed, along with metadata like the event type and timestamp. Inside your agent, every field from that payload is available as a variable you can pass to AI blocks, conditions, or other integrations.`, }, ] : []), @@ -156,7 +157,7 @@ export async function generateMetadata({ .slice(0, 3) .map((o) => o.name) .join(', ') - const metaDesc = `Automate ${name} with AI-powered workflows on Sim. ${description.slice(0, 100).trimEnd()}. Free to start.` + const metaDesc = `Automate ${name} with AI agents in Sim. ${description.slice(0, 100).trimEnd()}. Free to start.` return { title: `${name} Integration`, @@ -166,15 +167,15 @@ export async function generateMetadata({ `${name} integration`, `automate ${name}`, `connect ${name}`, - `${name} workflow`, + `${name} AI agent`, `${name} AI automation`, ...(opSample ? [`${name} ${opSample}`] : []), - 'workflow automation', - 'no-code automation', - 'AI agent workflow', + 'AI workspace integrations', + 'AI agent integrations', + 'AI agent builder', ], openGraph: { - title: `${name} Integration — AI Workflow Automation | Sim`, + title: `${name} Integration | Sim AI Workspace`, description: `Connect ${name} to ${INTEGRATION_COUNT - 1}+ tools using AI agents. ${description.slice(0, 100).trimEnd()}.`, url: `${baseUrl}/integrations/${slug}`, type: 'website', @@ -190,7 +191,7 @@ export async function generateMetadata({ twitter: { card: 'summary_large_image', title: `${name} Integration | Sim`, - description: `Automate ${name} with AI-powered workflows. Connect to ${INTEGRATION_COUNT - 1}+ tools. Free to start.`, + description: `Automate ${name} with AI agents in Sim. Connect to ${INTEGRATION_COUNT - 1}+ tools. Free to start.`, images: [{ url: `${baseUrl}/opengraph-image.png`, alt: `${name} Integration — Sim` }], }, alternates: { canonical: `${baseUrl}/integrations/${slug}` }, @@ -249,7 +250,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl '@context': 'https://schema.org', '@type': 'HowTo', name: `How to automate ${name} with Sim`, - description: `Step-by-step guide to connecting ${name} to AI-powered workflows in Sim.`, + description: `Step-by-step guide to connecting ${name} to AI agents in Sim.`, step: [ { '@type': 'HowToStep', @@ -261,13 +262,13 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl '@type': 'HowToStep', position: 2, name: `Add a ${name} block`, - text: `Open a workflow, drag a ${name} block onto the canvas, and authenticate with your ${name} credentials.`, + text: `Open your workspace, drag a ${name} block onto the workflow builder, and authenticate with your ${name} credentials.`, }, { '@type': 'HowToStep', position: 3, name: 'Configure and run', - text: `Choose the operation you want, connect it to an AI agent, and run your workflow. Automate anything in ${name} without code.`, + text: `Choose the operation you want, connect it to an AI agent, and deploy. Automate anything in ${name} without code.`, }, ], } @@ -366,12 +367,12 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl {/* CTAs */}
- Start building free - +

- Connect a {name} webhook to Sim and your workflow fires the instant an event happens - — no polling, no delay. + Connect a {name} webhook to Sim and your agent runs the instant an event happens — + no polling, no delay.

@@ -533,10 +534,10 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl id='templates-heading' className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em]' > - Workflow templates + Agent templates

- Ready-to-use workflows featuring {name}. Click any to build it instantly. + Ready-to-use templates featuring {name}. Click any to build it instantly.

@@ -775,15 +776,15 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl Start automating {name} today

- Build your first AI workflow with {name} in minutes. Connect to every tool your team - uses. Free to start — no credit card required. + Build your first AI agent with {name} in minutes. Connect to every tool your team uses. + Free to start — no credit card required.

- Build for free - +
diff --git a/apps/sim/app/(landing)/integrations/page.tsx b/apps/sim/app/(landing)/integrations/page.tsx index bdc67a537bd..60927489eeb 100644 --- a/apps/sim/app/(landing)/integrations/page.tsx +++ b/apps/sim/app/(landing)/integrations/page.tsx @@ -30,17 +30,17 @@ const featured = FEATURED_SLUGS.map((s) => bySlug.get(s)).filter( export const metadata: Metadata = { title: 'Integrations', - description: `Connect ${INTEGRATION_COUNT}+ apps and services with Sim's AI workflow automation. Build intelligent pipelines with ${TOP_NAMES.join(', ')}, and more.`, + description: `Connect ${INTEGRATION_COUNT}+ apps and services in Sim's AI workspace. Build agents that automate real work with ${TOP_NAMES.join(', ')}, and more.`, keywords: [ - 'workflow automation integrations', - 'AI workflow automation', - 'no-code automation', + 'AI workspace integrations', + 'AI agent integrations', + 'AI agent builder integrations', ...TOP_NAMES.flatMap((n) => [`${n} integration`, `${n} automation`]), ...allIntegrations.slice(0, 20).map((i) => `${i.name} automation`), ], openGraph: { - title: 'Integrations for AI Workflow Automation | Sim', - description: `Connect ${INTEGRATION_COUNT}+ apps with Sim. Build AI-powered pipelines that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`, + title: 'Integrations | Sim AI Workspace', + description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace. Build agents that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`, url: `${baseUrl}/integrations`, type: 'website', images: [ @@ -55,7 +55,7 @@ export const metadata: Metadata = { twitter: { card: 'summary_large_image', title: 'Integrations | Sim', - description: `Connect ${INTEGRATION_COUNT}+ apps with Sim's AI workflow automation.`, + description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace.`, images: [ { url: `${baseUrl}/opengraph-image.png`, alt: 'Sim Integrations for AI Workflow Automation' }, ], @@ -82,7 +82,7 @@ export default function IntegrationsPage() { '@context': 'https://schema.org', '@type': 'ItemList', name: 'Sim AI Workflow Integrations', - description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim for building AI-powered workflow automation.`, + description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim's AI workspace for building and deploying AI agents.`, url: `${baseUrl}/integrations`, numberOfItems: INTEGRATION_COUNT, itemListElement: allIntegrations.map((integration, index) => ({ @@ -129,7 +129,7 @@ export default function IntegrationsPage() { Integrations

- Connect every tool your team uses. Build AI-powered workflows that automate tasks across{' '} + Connect every tool your team uses. Build agents that automate real work across{' '} {INTEGRATION_COUNT} apps and services.

diff --git a/apps/sim/app/(landing)/landing-analytics.tsx b/apps/sim/app/(landing)/landing-analytics.tsx index d79e5faaa52..5a747b1306e 100644 --- a/apps/sim/app/(landing)/landing-analytics.tsx +++ b/apps/sim/app/(landing)/landing-analytics.tsx @@ -1,16 +1,13 @@ 'use client' import { useEffect } from 'react' -import { usePostHog } from 'posthog-js/react' -import { captureClientEvent, captureEvent } from '@/lib/posthog/client' +import { captureClientEvent } from '@/lib/posthog/client' import type { PostHogEventMap } from '@/lib/posthog/events' export function LandingAnalytics() { - const posthog = usePostHog() - useEffect(() => { - captureEvent(posthog, 'landing_page_viewed', {}) - }, [posthog]) + captureClientEvent('landing_page_viewed', {}) + }, []) return null } diff --git a/apps/sim/app/(landing)/landing.tsx b/apps/sim/app/(landing)/landing.tsx index fd4b8eafc77..8250d2e1a30 100644 --- a/apps/sim/app/(landing)/landing.tsx +++ b/apps/sim/app/(landing)/landing.tsx @@ -3,7 +3,6 @@ import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono' import { season } from '@/app/_styles/fonts/season/season' import { Collaboration, - // Enterprise, Features, Footer, Hero, @@ -31,7 +30,7 @@ import { LandingAnalytics } from '@/app/(landing)/landing-analytics' * - Section `id` attributes serve as fragment anchors for precise AI citations. * - Content ordering prioritizes answer-first patterns: definition (Hero) -> * examples (Templates) -> capabilities (Features) -> social proof (Collaboration) -> - * enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials). + * pricing (Pricing) -> testimonials (Testimonials). */ export default async function Landing() { const blogPosts = await getNavBlogPosts() @@ -53,16 +52,18 @@ export default async function Landing() {
- + - {/* */}
diff --git a/apps/sim/app/(landing)/models/page.tsx b/apps/sim/app/(landing)/models/page.tsx index f98b9f73ceb..dd73b1ea1c2 100644 --- a/apps/sim/app/(landing)/models/page.tsx +++ b/apps/sim/app/(landing)/models/page.tsx @@ -42,17 +42,18 @@ const faqItems = [ 'Tool use — also called function calling — lets an agent invoke external APIs, query databases, run code, or take any action you define. In Sim, all first-party models from OpenAI, Anthropic, Google, Mistral, Groq, Cerebras, and xAI support tool use. Look for the Tool Use capability tag on any model card in this directory to confirm support.', }, { - question: 'How do I add a model to a Sim agent workflow?', + question: 'How do I add a model to a Sim agent?', answer: - 'Open any workflow in Sim, add an Agent block, and select your provider and model from the model picker inside that block. Every model listed in this directory is available in the Agent block. Swapping models takes one click and does not affect the rest of your workflow, making it straightforward to test different models on the same task without rebuilding anything.', + 'Open Sim, add an Agent block, and select your provider and model from the model picker inside that block. Every model listed in this directory is available in the Agent block. Swapping models takes one click and does not affect the rest of your agent, making it straightforward to test different models on the same task without rebuilding anything.', }, ] export const metadata: Metadata = { title: 'AI Models Directory', - description: `Browse ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers. Compare pricing, context windows, and capabilities for OpenAI, Anthropic, Google, xAI, Mistral, Bedrock, Groq, and more.`, + description: `Browse and compare ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers in Sim's AI workspace. Compare pricing, context windows, and capabilities — then use any model directly in your agents.`, keywords: [ 'AI models directory', + 'AI model comparison', 'LLM model list', 'model pricing', 'context window comparison', @@ -185,7 +186,7 @@ export default function ModelsPage() { id='models-heading' className='text-balance text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]' > - Models + Compare AI Models

Browse {TOTAL_MODELS} AI models across {TOTAL_MODEL_PROVIDERS} providers. Compare diff --git a/apps/sim/app/(landing)/models/utils.ts b/apps/sim/app/(landing)/models/utils.ts index b0ff3260596..bb9a87e4ad3 100644 --- a/apps/sim/app/(landing)/models/utils.ts +++ b/apps/sim/app/(landing)/models/utils.ts @@ -695,7 +695,7 @@ export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel): { question: `What is the context window for ${model.displayName}?`, answer: model.contextWindow - ? `${model.displayName} supports a context window of ${formatTokenCount(model.contextWindow)} tokens in Sim. In an agent workflow, this determines how much conversation history, tool outputs, and retrieved documents the model can hold in a single call.` + ? `${model.displayName} supports a context window of ${formatTokenCount(model.contextWindow)} tokens in Sim. In an agent, this determines how much conversation history, tool outputs, and retrieved documents the model can hold in a single call.` : `A public context window value is not currently tracked for ${model.displayName}.`, }, { diff --git a/apps/sim/app/(landing)/partners/page.tsx b/apps/sim/app/(landing)/partners/page.tsx index 851d9486556..ccdda2603ee 100644 --- a/apps/sim/app/(landing)/partners/page.tsx +++ b/apps/sim/app/(landing)/partners/page.tsx @@ -8,7 +8,7 @@ import Navbar from '@/app/(landing)/components/navbar/navbar' export const metadata: Metadata = { title: 'Partner Program', description: - 'Join the Sim partner program. Build, deploy, and sell AI workflow solutions. Earn your certification through Sim Academy.', + "Join the Sim partner program. Build, deploy, and sell AI agent solutions powered by Sim's AI workspace. Earn your certification through Sim Academy.", metadataBase: new URL('https://sim.ai'), openGraph: { title: 'Partner Program | Sim', @@ -22,7 +22,7 @@ const PARTNER_TIERS = [ name: 'Certified Partner', badge: 'Entry', color: '#3A3A3A', - requirements: ['Complete Sim Academy certification', 'Deploy at least 1 live workflow'], + requirements: ['Complete Sim Academy certification', 'Deploy at least 1 live agent'], perks: [ 'Official partner badge', 'Listed in partner directory', @@ -69,13 +69,13 @@ const HOW_IT_WORKS = [ step: '01', title: 'Sign up & complete Sim Academy', description: - 'Create an account and work through the Sim Academy certification program. Learn to build, integrate, and deploy AI workflows through hands-on canvas exercises.', + 'Create an account and work through the Sim Academy certification program. Learn to build, integrate, and deploy AI agents through hands-on exercises.', }, { step: '02', title: 'Build & deploy real solutions', description: - 'Put your skills to work. Build workflow automations for clients, integrate Sim into existing products, or create your own Sim-powered applications.', + 'Put your skills to work. Build AI agents for clients, integrate Sim into existing products, or create your own Sim-powered applications.', }, { step: '03', @@ -119,7 +119,7 @@ const BENEFITS = [ icon: '📣', title: 'Community', description: - 'Join a growing community of Sim builders. Share workflows, collaborate on solutions, and shape the product roadmap.', + 'Join a growing community of Sim builders. Share agents, collaborate on solutions, and shape the product roadmap.', }, ] @@ -144,11 +144,11 @@ export default async function PartnersPage() {

Build the future
- of AI automation + of AI agents

Become a certified Sim partner. Complete Sim Academy, deploy real solutions, and earn - recognition in the growing ecosystem of AI workflow builders. + recognition in the growing ecosystem of AI agent builders.

{/* TODO: Uncomment when academy is public */} diff --git a/apps/sim/app/api/audit-logs/route.ts b/apps/sim/app/api/audit-logs/route.ts new file mode 100644 index 00000000000..3be8c2dc3b6 --- /dev/null +++ b/apps/sim/app/api/audit-logs/route.ts @@ -0,0 +1,71 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth' +import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format' +import { + buildFilterConditions, + buildOrgScopeCondition, + queryAuditLogs, +} from '@/app/api/v1/audit-logs/query' + +const logger = createLogger('AuditLogsAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: Request) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const authResult = await validateEnterpriseAuditAccess(session.user.id) + if (!authResult.success) { + return authResult.response + } + + const { orgMemberIds } = authResult.context + + const { searchParams } = new URL(request.url) + const search = searchParams.get('search')?.trim() || undefined + const startDate = searchParams.get('startDate') || undefined + const endDate = searchParams.get('endDate') || undefined + const includeDeparted = searchParams.get('includeDeparted') === 'true' + const limit = Math.min(Math.max(Number(searchParams.get('limit')) || 50, 1), 100) + const cursor = searchParams.get('cursor') || undefined + + if (startDate && Number.isNaN(Date.parse(startDate))) { + return NextResponse.json({ error: 'Invalid startDate format' }, { status: 400 }) + } + if (endDate && Number.isNaN(Date.parse(endDate))) { + return NextResponse.json({ error: 'Invalid endDate format' }, { status: 400 }) + } + + const scopeCondition = await buildOrgScopeCondition(orgMemberIds, includeDeparted) + const filterConditions = buildFilterConditions({ + action: searchParams.get('action') || undefined, + resourceType: searchParams.get('resourceType') || undefined, + actorId: searchParams.get('actorId') || undefined, + search, + startDate, + endDate, + }) + + const { data, nextCursor } = await queryAuditLogs( + [scopeCondition, ...filterConditions], + limit, + cursor + ) + + return NextResponse.json({ + success: true, + data: data.map(formatAuditLogEntry), + nextCursor, + }) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error('Audit logs fetch error', { error: message }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/auth/forget-password/route.ts b/apps/sim/app/api/auth/forget-password/route.ts index e8f05ecfcf1..9db6eef95c9 100644 --- a/apps/sim/app/api/auth/forget-password/route.ts +++ b/apps/sim/app/api/auth/forget-password/route.ts @@ -1,6 +1,10 @@ +import { db } from '@sim/db' +import { user } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { auth } from '@/lib/auth' import { isSameOrigin } from '@/lib/core/utils/validation' @@ -51,6 +55,26 @@ export async function POST(request: NextRequest) { method: 'POST', }) + const [existingUser] = await db + .select({ id: user.id, name: user.name, email: user.email }) + .from(user) + .where(eq(user.email, email)) + .limit(1) + + if (existingUser) { + recordAudit({ + actorId: existingUser.id, + actorName: existingUser.name, + actorEmail: existingUser.email, + action: AuditAction.PASSWORD_RESET_REQUESTED, + resourceType: AuditResourceType.PASSWORD, + resourceId: existingUser.id, + resourceName: existingUser.email ?? undefined, + description: `Password reset requested for ${existingUser.email}`, + request, + }) + } + return NextResponse.json({ success: true }) } catch (error) { logger.error('Error requesting password reset:', { error }) diff --git a/apps/sim/app/api/auth/providers/route.ts b/apps/sim/app/api/auth/providers/route.ts new file mode 100644 index 00000000000..dadd3fcd083 --- /dev/null +++ b/apps/sim/app/api/auth/providers/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server' +import { isRegistrationDisabled } from '@/lib/core/config/feature-flags' +import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker' + +export const dynamic = 'force-dynamic' + +export async function GET() { + const { githubAvailable, googleAvailable } = await getOAuthProviderStatus() + return NextResponse.json({ + githubAvailable, + googleAvailable, + registrationDisabled: isRegistrationDisabled, + }) +} diff --git a/apps/sim/app/api/billing/credits/route.ts b/apps/sim/app/api/billing/credits/route.ts index 7dfeafb2efe..070b3893133 100644 --- a/apps/sim/app/api/billing/credits/route.ts +++ b/apps/sim/app/api/billing/credits/route.ts @@ -64,8 +64,12 @@ export async function POST(request: NextRequest) { actorEmail: session.user.email, action: AuditAction.CREDIT_PURCHASED, resourceType: AuditResourceType.BILLING, + resourceId: validation.data.requestId, description: `Purchased $${validation.data.amount} in credits`, - metadata: { amount: validation.data.amount, requestId: validation.data.requestId }, + metadata: { + amountDollars: validation.data.amount, + requestId: validation.data.requestId, + }, request, }) diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts index c09688c99d6..8cf37410ae0 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.ts @@ -233,6 +233,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< resourceId: chatId, resourceName: title || existingChatRecord.title, description: `Updated chat deployment "${title || existingChatRecord.title}"`, + metadata: { + identifier: updatedIdentifier, + authType: updateData.authType || existingChatRecord.authType, + workflowId: workflowId || existingChatRecord.workflowId, + chatUrl, + }, request, }) diff --git a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts index 9a91b86b8e2..752ebc1a9e7 100644 --- a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts @@ -159,7 +159,12 @@ export async function POST( resourceId: id, resourceName: result.set.name, description: `Resent credential set invitation to ${invitation.email}`, - metadata: { invitationId, targetEmail: invitation.email }, + metadata: { + invitationId, + targetEmail: invitation.email, + providerId: result.set.providerId, + credentialSetName: result.set.name, + }, request: req, }) diff --git a/apps/sim/app/api/credential-sets/[id]/invite/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/route.ts index cd5ebb53015..b9b0ccc4a95 100644 --- a/apps/sim/app/api/credential-sets/[id]/invite/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/invite/route.ts @@ -187,7 +187,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: actorEmail: session.user.email ?? undefined, resourceName: result.set.name, description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`, - metadata: { targetEmail: email || undefined }, + metadata: { + invitationId: invitation.id, + targetEmail: email || undefined, + providerId: result.set.providerId, + credentialSetName: result.set.name, + }, request: req, }) diff --git a/apps/sim/app/api/credential-sets/[id]/members/route.ts b/apps/sim/app/api/credential-sets/[id]/members/route.ts index e6ffbaa6262..8ec89923bbe 100644 --- a/apps/sim/app/api/credential-sets/[id]/members/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/members/route.ts @@ -197,7 +197,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i actorEmail: session.user.email ?? undefined, resourceName: result.set.name, description: `Removed member from credential set "${result.set.name}"`, - metadata: { targetEmail: memberToRemove.email ?? undefined }, + metadata: { + memberId, + memberUserId: memberToRemove.userId, + targetEmail: memberToRemove.email ?? undefined, + providerId: result.set.providerId, + }, request: req, }) diff --git a/apps/sim/app/api/credential-sets/[id]/route.ts b/apps/sim/app/api/credential-sets/[id]/route.ts index 51110916e93..d522cf9c3df 100644 --- a/apps/sim/app/api/credential-sets/[id]/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/route.ts @@ -142,6 +142,13 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: actorEmail: session.user.email ?? undefined, resourceName: updated?.name ?? result.set.name, description: `Updated credential set "${updated?.name ?? result.set.name}"`, + metadata: { + organizationId: result.set.organizationId, + providerId: result.set.providerId, + updatedFields: Object.keys(updates).filter( + (k) => updates[k as keyof typeof updates] !== undefined + ), + }, request: req, }) @@ -199,6 +206,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i actorEmail: session.user.email ?? undefined, resourceName: result.set.name, description: `Deleted credential set "${result.set.name}"`, + metadata: { organizationId: result.set.organizationId, providerId: result.set.providerId }, request: req, }) diff --git a/apps/sim/app/api/credential-sets/invite/[token]/route.ts b/apps/sim/app/api/credential-sets/invite/[token]/route.ts index 656d39fdde1..fc3759b0e27 100644 --- a/apps/sim/app/api/credential-sets/invite/[token]/route.ts +++ b/apps/sim/app/api/credential-sets/invite/[token]/route.ts @@ -192,7 +192,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok resourceId: invitation.credentialSetId, resourceName: invitation.credentialSetName, description: `Accepted credential set invitation`, - metadata: { invitationId: invitation.id }, + metadata: { + invitationId: invitation.id, + credentialSetId: invitation.credentialSetId, + providerId: invitation.providerId, + credentialSetName: invitation.credentialSetName, + }, request: req, }) diff --git a/apps/sim/app/api/credential-sets/memberships/route.ts b/apps/sim/app/api/credential-sets/memberships/route.ts index aef704f7b9c..926714b98f9 100644 --- a/apps/sim/app/api/credential-sets/memberships/route.ts +++ b/apps/sim/app/api/credential-sets/memberships/route.ts @@ -116,6 +116,7 @@ export async function DELETE(req: NextRequest) { resourceType: AuditResourceType.CREDENTIAL_SET, resourceId: credentialSetId, description: `Left credential set`, + metadata: { credentialSetId }, request: req, }) diff --git a/apps/sim/app/api/credential-sets/route.ts b/apps/sim/app/api/credential-sets/route.ts index b5166630af9..c120e84b421 100644 --- a/apps/sim/app/api/credential-sets/route.ts +++ b/apps/sim/app/api/credential-sets/route.ts @@ -179,6 +179,7 @@ export async function POST(req: Request) { actorEmail: session.user.email ?? undefined, resourceName: name, description: `Created credential set "${name}"`, + metadata: { organizationId, providerId, credentialSetName: name }, request: req, }) diff --git a/apps/sim/app/api/credentials/[id]/route.ts b/apps/sim/app/api/credentials/[id]/route.ts index 14f2e73142b..c3a61569051 100644 --- a/apps/sim/app/api/credentials/[id]/route.ts +++ b/apps/sim/app/api/credentials/[id]/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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { generateId } from '@/lib/core/utils/uuid' @@ -166,6 +167,23 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ updates.updatedAt = new Date() await db.update(credential).set(updates).where(eq(credential.id, id)) + recordAudit({ + workspaceId: access.credential.workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.CREDENTIAL_UPDATED, + resourceType: AuditResourceType.CREDENTIAL, + resourceId: id, + resourceName: access.credential.displayName, + description: `Updated ${access.credential.type} credential "${access.credential.displayName}"`, + metadata: { + credentialType: access.credential.type, + updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'), + }, + request, + }) + const row = await getCredentialResponse(id, session.user.id) return NextResponse.json({ credential: row }, { status: 200 }) } catch (error) { @@ -249,6 +267,20 @@ export async function DELETE( { groups: { workspace: access.credential.workspaceId } } ) + recordAudit({ + workspaceId: access.credential.workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.CREDENTIAL_DELETED, + resourceType: AuditResourceType.CREDENTIAL, + resourceId: id, + resourceName: access.credential.displayName, + description: `Deleted personal env credential "${access.credential.envKey}"`, + metadata: { credentialType: 'env_personal', envKey: access.credential.envKey }, + request, + }) + return NextResponse.json({ success: true }, { status: 200 }) } @@ -302,6 +334,20 @@ export async function DELETE( { groups: { workspace: access.credential.workspaceId } } ) + recordAudit({ + workspaceId: access.credential.workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.CREDENTIAL_DELETED, + resourceType: AuditResourceType.CREDENTIAL, + resourceId: id, + resourceName: access.credential.displayName, + description: `Deleted workspace env credential "${access.credential.envKey}"`, + metadata: { credentialType: 'env_workspace', envKey: access.credential.envKey }, + request, + }) + return NextResponse.json({ success: true }, { status: 200 }) } @@ -318,6 +364,23 @@ export async function DELETE( { groups: { workspace: access.credential.workspaceId } } ) + recordAudit({ + workspaceId: access.credential.workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.CREDENTIAL_DELETED, + resourceType: AuditResourceType.CREDENTIAL, + resourceId: id, + resourceName: access.credential.displayName, + description: `Deleted ${access.credential.type} credential "${access.credential.displayName}"`, + metadata: { + credentialType: access.credential.type, + providerId: access.credential.providerId, + }, + request, + }) + return NextResponse.json({ success: true }, { status: 200 }) } catch (error) { logger.error('Failed to delete credential', error) diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index 7d30b63d7b4..0b210325064 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' @@ -612,6 +613,23 @@ export async function POST(request: NextRequest) { } ) + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.CREDENTIAL_CREATED, + resourceType: AuditResourceType.CREDENTIAL, + resourceId: credentialId, + resourceName: resolvedDisplayName, + description: `Created ${type} credential "${resolvedDisplayName}"`, + metadata: { + credentialType: type, + providerId: resolvedProviderId, + }, + request, + }) + return NextResponse.json({ credential: created }, { status: 201 }) } catch (error: any) { if (error?.code === '23505') { diff --git a/apps/sim/app/api/environment/route.ts b/apps/sim/app/api/environment/route.ts index 229ba26382f..f8167e92ac2 100644 --- a/apps/sim/app/api/environment/route.ts +++ b/apps/sim/app/api/environment/route.ts @@ -67,8 +67,13 @@ export async function POST(req: NextRequest) { actorEmail: session.user.email, action: AuditAction.ENVIRONMENT_UPDATED, resourceType: AuditResourceType.ENVIRONMENT, - description: 'Updated global environment variables', - metadata: { variableCount: Object.keys(variables).length }, + resourceId: session.user.id, + description: `Updated ${Object.keys(variables).length} personal environment variable(s)`, + metadata: { + variableCount: Object.keys(variables).length, + updatedKeys: Object.keys(variables), + scope: 'personal', + }, request: req, }) diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts index 98e80f5aa3d..37e0ae8d1d8 100644 --- a/apps/sim/app/api/folders/route.ts +++ b/apps/sim/app/api/folders/route.ts @@ -168,7 +168,13 @@ export async function POST(request: NextRequest) { resourceId: id, resourceName: name.trim(), description: `Created folder "${name.trim()}"`, - metadata: { name: name.trim() }, + metadata: { + name: name.trim(), + workspaceId, + parentId: parentId || undefined, + color: color || '#6B7280', + sortOrder: newFolder.sortOrder, + }, request, }) diff --git a/apps/sim/app/api/form/manage/[id]/route.ts b/apps/sim/app/api/form/manage/[id]/route.ts index 577363b8d9c..a57a7c937bb 100644 --- a/apps/sim/app/api/form/manage/[id]/route.ts +++ b/apps/sim/app/api/form/manage/[id]/route.ts @@ -197,8 +197,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< resourceId: id, actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, - resourceName: formRecord.title ?? undefined, - description: `Updated form "${formRecord.title}"`, + resourceName: (title || formRecord.title) ?? undefined, + description: `Updated form "${title || formRecord.title}"`, + metadata: { + identifier: identifier || formRecord.identifier, + workflowId: formRecord.workflowId, + authType: authType || formRecord.authType, + updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'), + }, request, }) @@ -255,6 +261,7 @@ export async function DELETE( actorEmail: session.user.email ?? undefined, resourceName: formRecord.title ?? undefined, description: `Deleted form "${formRecord.title}"`, + metadata: { identifier: formRecord.identifier, workflowId: formRecord.workflowId }, request, }) diff --git a/apps/sim/app/api/form/route.ts b/apps/sim/app/api/form/route.ts index 6512ba95808..db29c2759de 100644 --- a/apps/sim/app/api/form/route.ts +++ b/apps/sim/app/api/form/route.ts @@ -208,6 +208,7 @@ export async function POST(request: NextRequest) { actorEmail: session.user.email ?? undefined, resourceName: title, description: `Created form "${title}" for workflow ${workflowId}`, + metadata: { identifier, workflowId, authType, formUrl, showBranding }, request, }) diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts index 48e0d0deb2d..c5e7878fc69 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts @@ -194,7 +194,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { resourceType: AuditResourceType.CONNECTOR, resourceId: connectorId, description: `Restored ${updated.length} excluded document(s) for knowledge base "${writeCheck.knowledgeBase.name}"`, - metadata: { knowledgeBaseId, documentCount: updated.length }, + metadata: { + knowledgeBaseId, + knowledgeBaseName: writeCheck.knowledgeBase.name, + operation: 'restore', + documentCount: updated.length, + documentIds: updated.map((d) => d.id), + }, request, }) @@ -229,7 +235,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { resourceType: AuditResourceType.CONNECTOR, resourceId: connectorId, description: `Excluded ${updated.length} document(s) from knowledge base "${writeCheck.knowledgeBase.name}"`, - metadata: { knowledgeBaseId, documentCount: updated.length }, + metadata: { + knowledgeBaseId, + knowledgeBaseName: writeCheck.knowledgeBase.name, + operation: 'exclude', + documentCount: updated.length, + documentIds: updated.map((d) => d.id), + }, request, }) diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts index 87cdb51a737..6ffee2355a1 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts @@ -268,7 +268,16 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { resourceId: connectorId, resourceName: updatedData.connectorType, description: `Updated connector for knowledge base "${writeCheck.knowledgeBase.name}"`, - metadata: { knowledgeBaseId, updatedFields: Object.keys(parsed.data) }, + metadata: { + knowledgeBaseId, + knowledgeBaseName: writeCheck.knowledgeBase.name, + connectorType: updatedData.connectorType, + updatedFields: Object.keys(parsed.data), + ...(parsed.data.syncIntervalMinutes !== undefined && { + syncIntervalMinutes: parsed.data.syncIntervalMinutes, + }), + ...(parsed.data.status !== undefined && { newStatus: parsed.data.status }), + }, request, }) @@ -399,6 +408,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { description: `Deleted connector from knowledge base "${writeCheck.knowledgeBase.name}"`, metadata: { knowledgeBaseId, + knowledgeBaseName: writeCheck.knowledgeBase.name, + connectorType: existingConnector[0].connectorType, + deleteDocuments, documentsDeleted: deleteDocuments ? docCount : 0, documentsKept: deleteDocuments ? 0 : docCount, }, diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts index df7057fc904..1ace24c886b 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts @@ -78,7 +78,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) { resourceId: connectorId, resourceName: connectorRows[0].connectorType, description: `Triggered manual sync for connector on knowledge base "${writeCheck.knowledgeBase.name}"`, - metadata: { knowledgeBaseId }, + metadata: { + knowledgeBaseId, + knowledgeBaseName: writeCheck.knowledgeBase.name, + connectorType: connectorRows[0].connectorType, + connectorStatus: connectorRows[0].status, + syncType: 'manual', + }, request, }) diff --git a/apps/sim/app/api/knowledge/[id]/connectors/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/route.ts index b5e2cb86f46..34da8e03276 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/route.ts @@ -286,7 +286,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ resourceId: connectorId, resourceName: connectorType, description: `Created ${connectorType} connector for knowledge base "${writeCheck.knowledgeBase.name}"`, - metadata: { knowledgeBaseId, connectorType, syncIntervalMinutes }, + metadata: { + knowledgeBaseId, + knowledgeBaseName: writeCheck.knowledgeBase.name, + connectorType, + syncIntervalMinutes, + authMode: connectorConfig.auth.mode, + }, request, }) diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts index 4f8735826b1..f238ac4f978 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts @@ -208,7 +208,16 @@ export async function PUT( resourceType: AuditResourceType.DOCUMENT, resourceId: documentId, resourceName: validatedData.filename ?? accessCheck.document?.filename, - description: `Updated document "${documentId}" in knowledge base "${knowledgeBaseId}"`, + description: `Updated document "${validatedData.filename ?? accessCheck.document?.filename}" in knowledge base "${knowledgeBaseId}"`, + metadata: { + knowledgeBaseId, + knowledgeBaseName: accessCheck.knowledgeBase?.name, + fileName: validatedData.filename ?? accessCheck.document?.filename, + updatedFields: Object.keys(validatedData).filter( + (k) => validatedData[k as keyof typeof validatedData] !== undefined + ), + ...(validatedData.enabled !== undefined && { enabled: validatedData.enabled }), + }, request: req, }) @@ -281,8 +290,14 @@ export async function DELETE( resourceType: AuditResourceType.DOCUMENT, resourceId: documentId, resourceName: accessCheck.document?.filename, - description: `Deleted document "${documentId}" from knowledge base "${knowledgeBaseId}"`, - metadata: { fileName: accessCheck.document?.filename }, + description: `Deleted document "${accessCheck.document?.filename}" from knowledge base "${knowledgeBaseId}"`, + metadata: { + knowledgeBaseId, + knowledgeBaseName: accessCheck.knowledgeBase?.name, + fileName: accessCheck.document?.filename, + fileSize: accessCheck.document?.fileSize, + mimeType: accessCheck.document?.mimeType, + }, request: req, }) diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index 83056e8f486..b5614aec41d 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -278,8 +278,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: resourceName: `${createdDocuments.length} document(s)`, description: `Uploaded ${createdDocuments.length} document(s) to knowledge base "${knowledgeBaseId}"`, metadata: { + knowledgeBaseName: accessCheck.knowledgeBase?.name, fileCount: createdDocuments.length, - fileNames: createdDocuments.map((doc) => doc.filename), }, request: req, }) @@ -358,6 +358,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: resourceName: validatedData.filename, description: `Uploaded document "${validatedData.filename}" to knowledge base "${knowledgeBaseId}"`, metadata: { + knowledgeBaseName: accessCheck.knowledgeBase?.name, fileName: validatedData.filename, fileType: validatedData.mimeType, fileSize: validatedData.fileSize, diff --git a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts index 59be57cd610..8d5ee153918 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts @@ -196,7 +196,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: ? `Upserted (replaced) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"` : `Upserted (created) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`, metadata: { + knowledgeBaseName: accessCheck.knowledgeBase?.name, fileName: validatedData.filename, + fileType: validatedData.mimeType, + fileSize: validatedData.fileSize, previousDocumentId: existingDocumentId, isUpdate, }, diff --git a/apps/sim/app/api/knowledge/[id]/restore/route.ts b/apps/sim/app/api/knowledge/[id]/restore/route.ts index 1d37f664ab7..02d8b3e5afd 100644 --- a/apps/sim/app/api/knowledge/[id]/restore/route.ts +++ b/apps/sim/app/api/knowledge/[id]/restore/route.ts @@ -59,6 +59,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ resourceId: id, resourceName: kb.name, description: `Restored knowledge base "${kb.name}"`, + metadata: { + knowledgeBaseName: kb.name, + }, request, }) diff --git a/apps/sim/app/api/knowledge/[id]/route.ts b/apps/sim/app/api/knowledge/[id]/route.ts index 2dcf53701da..5da7026a454 100644 --- a/apps/sim/app/api/knowledge/[id]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/route.ts @@ -147,6 +147,20 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: resourceId: id, resourceName: validatedData.name ?? updatedKnowledgeBase.name, description: `Updated knowledge base "${validatedData.name ?? updatedKnowledgeBase.name}"`, + metadata: { + updatedFields: Object.keys(validatedData).filter( + (k) => validatedData[k as keyof typeof validatedData] !== undefined + ), + ...(validatedData.name && { newName: validatedData.name }), + ...(validatedData.description !== undefined && { + description: validatedData.description, + }), + ...(validatedData.chunkingConfig && { + chunkMaxSize: validatedData.chunkingConfig.maxSize, + chunkMinSize: validatedData.chunkingConfig.minSize, + chunkOverlap: validatedData.chunkingConfig.overlap, + }), + }, request: req, }) @@ -226,6 +240,9 @@ export async function DELETE( resourceId: id, resourceName: accessCheck.knowledgeBase.name, description: `Deleted knowledge base "${accessCheck.knowledgeBase.name || id}"`, + metadata: { + knowledgeBaseName: accessCheck.knowledgeBase.name, + }, request: _request, }) diff --git a/apps/sim/app/api/knowledge/route.ts b/apps/sim/app/api/knowledge/route.ts index 20499ce8fce..9641f3a4539 100644 --- a/apps/sim/app/api/knowledge/route.ts +++ b/apps/sim/app/api/knowledge/route.ts @@ -162,7 +162,16 @@ export async function POST(req: NextRequest) { resourceId: newKnowledgeBase.id, resourceName: validatedData.name, description: `Created knowledge base "${validatedData.name}"`, - metadata: { name: validatedData.name }, + metadata: { + name: validatedData.name, + description: validatedData.description, + embeddingModel: validatedData.embeddingModel, + embeddingDimension: validatedData.embeddingDimension, + chunkingStrategy: validatedData.chunkingConfig.strategy, + chunkMaxSize: validatedData.chunkingConfig.maxSize, + chunkMinSize: validatedData.chunkingConfig.minSize, + chunkOverlap: validatedData.chunkingConfig.overlap, + }, request: req, }) diff --git a/apps/sim/app/api/mcp/servers/[id]/route.ts b/apps/sim/app/api/mcp/servers/[id]/route.ts index 54265bb687c..67c893fc754 100644 --- a/apps/sim/app/api/mcp/servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/servers/[id]/route.ts @@ -124,6 +124,14 @@ export const PATCH = withMcpAuth<{ id: string }>('write')( resourceId: serverId, resourceName: updatedServer.name || serverId, description: `Updated MCP server "${updatedServer.name || serverId}"`, + metadata: { + serverName: updatedServer.name, + transport: updatedServer.transport, + url: updatedServer.url, + updatedFields: Object.keys(updateData).filter( + (k) => k !== 'workspaceId' && k !== 'updatedAt' + ), + }, request, }) diff --git a/apps/sim/app/api/mcp/servers/route.ts b/apps/sim/app/api/mcp/servers/route.ts index 054c7a3a2ca..5d5c1d8b6fe 100644 --- a/apps/sim/app/api/mcp/servers/route.ts +++ b/apps/sim/app/api/mcp/servers/route.ts @@ -206,7 +206,14 @@ export const POST = withMcpAuth('write')( resourceId: serverId, resourceName: body.name, description: `Added MCP server "${body.name}"`, - metadata: { serverName: body.name, transport: body.transport }, + metadata: { + serverName: body.name, + transport: body.transport, + url: body.url, + timeout: body.timeout || 30000, + retries: body.retries || 3, + source: source, + }, request, }) @@ -278,6 +285,12 @@ export const DELETE = withMcpAuth('admin')( resourceId: serverId!, resourceName: deletedServer.name, description: `Removed MCP server "${deletedServer.name}"`, + metadata: { + serverName: deletedServer.name, + transport: deletedServer.transport, + url: deletedServer.url, + source, + }, request, }) 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 f5ed5371e19..6ed1bb2e0c6 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts @@ -135,6 +135,11 @@ export const PATCH = withMcpAuth('write')( resourceId: serverId, resourceName: updatedServer.name, description: `Updated workflow MCP server "${updatedServer.name}"`, + metadata: { + serverName: updatedServer.name, + isPublic: updatedServer.isPublic, + updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'), + }, request, }) @@ -189,6 +194,7 @@ export const DELETE = withMcpAuth('admin')( resourceId: serverId, resourceName: deletedServer.name, description: `Unpublished workflow MCP server "${deletedServer.name}"`, + metadata: { serverName: deletedServer.name }, request, }) 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 f54caf4703e..60791a36bcf 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 @@ -152,7 +152,12 @@ export const PATCH = withMcpAuth('write')( resourceType: AuditResourceType.MCP_SERVER, resourceId: serverId, description: `Updated tool "${updatedTool.toolName}" in MCP server`, - metadata: { toolId, toolName: updatedTool.toolName }, + metadata: { + toolId, + toolName: updatedTool.toolName, + workflowId: updatedTool.workflowId, + updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'), + }, request, }) @@ -220,7 +225,7 @@ export const DELETE = withMcpAuth('write')( resourceType: AuditResourceType.MCP_SERVER, resourceId: serverId, description: `Removed tool "${deletedTool.toolName}" from MCP server`, - metadata: { toolId, toolName: deletedTool.toolName }, + metadata: { toolId, toolName: deletedTool.toolName, workflowId: deletedTool.workflowId }, request, }) 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 1a4687b44fc..396cfe92468 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 @@ -224,7 +224,13 @@ export const POST = withMcpAuth('write')( resourceType: AuditResourceType.MCP_SERVER, resourceId: serverId, description: `Added tool "${toolName}" to MCP server`, - metadata: { toolId, toolName, workflowId: body.workflowId }, + metadata: { + toolId, + toolName, + toolDescription, + workflowId: body.workflowId, + workflowName: workflowRecord.name, + }, request, }) diff --git a/apps/sim/app/api/mcp/workflow-servers/route.ts b/apps/sim/app/api/mcp/workflow-servers/route.ts index 84d431fa423..807df769673 100644 --- a/apps/sim/app/api/mcp/workflow-servers/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/route.ts @@ -208,6 +208,13 @@ export const POST = withMcpAuth('write')( resourceId: serverId, resourceName: body.name.trim(), description: `Published workflow MCP server "${body.name.trim()}" with ${addedTools.length} tool(s)`, + metadata: { + serverName: body.name.trim(), + isPublic: body.isPublic ?? false, + toolCount: addedTools.length, + toolNames: addedTools.map((t) => t.toolName), + workflowIds: addedTools.map((t) => t.workflowId), + }, request, }) diff --git a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts index f54e72b2701..7f4f7d8004c 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts @@ -182,6 +182,20 @@ export async function POST( email: orgInvitation.email, }) + recordAudit({ + workspaceId: null, + actorId: session.user.id, + action: AuditAction.ORG_INVITATION_RESENT, + resourceType: AuditResourceType.ORGANIZATION, + resourceId: organizationId, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + resourceName: org?.name ?? undefined, + description: `Resent organization invitation to ${orgInvitation.email}`, + metadata: { invitationId, targetEmail: orgInvitation.email, targetRole: orgInvitation.role }, + request: _request, + }) + return NextResponse.json({ success: true, message: 'Invitation resent successfully', diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts index 001184d98e7..5a85cfbb4f0 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts @@ -423,7 +423,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ actorEmail: session.user.email ?? undefined, resourceName: organizationEntry[0]?.name, description: `Invited ${inv.email} to organization as ${role}`, - metadata: { invitationId: inv.id, targetEmail: inv.email, targetRole: role }, + metadata: { + invitationId: inv.id, + targetEmail: inv.email, + targetRole: role, + isBatch, + workspaceInvitationCount: validWorkspaceInvitations.length, + }, request, }) } @@ -558,7 +564,7 @@ export async function DELETE( actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, description: `Revoked organization invitation for ${result[0].email}`, - metadata: { invitationId, targetEmail: result[0].email }, + metadata: { invitationId, targetEmail: result[0].email, targetRole: result[0].role }, request, }) diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index 3b15d34848e..989d792b6fd 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -294,6 +294,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ resourceId: organizationId, actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, + resourceName: organizationEntry[0]?.name ?? undefined, description: `Invited ${normalizedEmail} to organization as ${role}`, metadata: { invitationId, targetEmail: normalizedEmail, targetRole: role }, request, diff --git a/apps/sim/app/api/organizations/route.ts b/apps/sim/app/api/organizations/route.ts index 5803f85dc25..6185f120f46 100644 --- a/apps/sim/app/api/organizations/route.ts +++ b/apps/sim/app/api/organizations/route.ts @@ -126,6 +126,7 @@ export async function POST(request: Request) { actorEmail: user.email ?? undefined, resourceName: organizationName ?? undefined, description: `Created organization "${organizationName}"`, + metadata: { organizationSlug }, request, }) diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts index 9391b67d826..7cab684f043 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -21,7 +21,10 @@ const configSchema = z.object({ hideKnowledgeBaseTab: z.boolean().optional(), hideTablesTab: z.boolean().optional(), hideCopilot: z.boolean().optional(), + hideIntegrationsTab: z.boolean().optional(), + hideSecretsTab: z.boolean().optional(), hideApiKeysTab: z.boolean().optional(), + hideInboxTab: z.boolean().optional(), hideEnvironmentTab: z.boolean().optional(), hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), @@ -29,6 +32,7 @@ const configSchema = z.object({ disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), + disablePublicApi: z.boolean().optional(), hideDeployApi: z.boolean().optional(), hideDeployMcp: z.boolean().optional(), hideDeployA2a: z.boolean().optional(), @@ -151,31 +155,34 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: ? { ...currentConfig, ...updates.config } : currentConfig - // If setting autoAddNewMembers to true, unset it on other groups in the org first - if (updates.autoAddNewMembers === true) { - await db - .update(permissionGroup) - .set({ autoAddNewMembers: false, updatedAt: new Date() }) - .where( - and( - eq(permissionGroup.organizationId, result.group.organizationId), - eq(permissionGroup.autoAddNewMembers, true) + const now = new Date() + + await db.transaction(async (tx) => { + if (updates.autoAddNewMembers === true) { + await tx + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: now }) + .where( + and( + eq(permissionGroup.organizationId, result.group.organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) ) - ) - } + } - await db - .update(permissionGroup) - .set({ - ...(updates.name !== undefined && { name: updates.name }), - ...(updates.description !== undefined && { description: updates.description }), - ...(updates.autoAddNewMembers !== undefined && { - autoAddNewMembers: updates.autoAddNewMembers, - }), - config: newConfig, - updatedAt: new Date(), - }) - .where(eq(permissionGroup.id, id)) + await tx + .update(permissionGroup) + .set({ + ...(updates.name !== undefined && { name: updates.name }), + ...(updates.description !== undefined && { description: updates.description }), + ...(updates.autoAddNewMembers !== undefined && { + autoAddNewMembers: updates.autoAddNewMembers, + }), + config: newConfig, + updatedAt: now, + }) + .where(eq(permissionGroup.id, id)) + }) const [updated] = await db .select() @@ -193,6 +200,12 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: actorEmail: session.user.email ?? undefined, resourceName: updated.name, description: `Updated permission group "${updated.name}"`, + metadata: { + organizationId: result.group.organizationId, + updatedFields: Object.keys(updates).filter( + (k) => updates[k as keyof typeof updates] !== undefined + ), + }, request: req, }) @@ -239,8 +252,10 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) } - await db.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id)) - await db.delete(permissionGroup).where(eq(permissionGroup.id, id)) + await db.transaction(async (tx) => { + await tx.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id)) + await tx.delete(permissionGroup).where(eq(permissionGroup.id, id)) + }) logger.info('Deleted permission group', { permissionGroupId: id, userId: session.user.id }) @@ -254,6 +269,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i actorEmail: session.user.email ?? undefined, resourceName: result.group.name, description: `Deleted permission group "${result.group.name}"`, + metadata: { organizationId: result.group.organizationId }, request: req, }) diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts index b79f01ebc5f..dd5c09e5453 100644 --- a/apps/sim/app/api/permission-groups/route.ts +++ b/apps/sim/app/api/permission-groups/route.ts @@ -23,7 +23,10 @@ const configSchema = z.object({ hideKnowledgeBaseTab: z.boolean().optional(), hideTablesTab: z.boolean().optional(), hideCopilot: z.boolean().optional(), + hideIntegrationsTab: z.boolean().optional(), + hideSecretsTab: z.boolean().optional(), hideApiKeysTab: z.boolean().optional(), + hideInboxTab: z.boolean().optional(), hideEnvironmentTab: z.boolean().optional(), hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), @@ -31,6 +34,7 @@ const configSchema = z.object({ disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), + disablePublicApi: z.boolean().optional(), hideDeployApi: z.boolean().optional(), hideDeployMcp: z.boolean().optional(), hideDeployA2a: z.boolean().optional(), @@ -167,19 +171,6 @@ export async function POST(req: Request) { ...config, } - // If autoAddNewMembers is true, unset it on any existing groups first - if (autoAddNewMembers) { - await db - .update(permissionGroup) - .set({ autoAddNewMembers: false, updatedAt: new Date() }) - .where( - and( - eq(permissionGroup.organizationId, organizationId), - eq(permissionGroup.autoAddNewMembers, true) - ) - ) - } - const now = new Date() const newGroup = { id: generateId(), @@ -193,7 +184,20 @@ export async function POST(req: Request) { autoAddNewMembers: autoAddNewMembers || false, } - await db.insert(permissionGroup).values(newGroup) + await db.transaction(async (tx) => { + if (autoAddNewMembers) { + await tx + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: now }) + .where( + and( + eq(permissionGroup.organizationId, organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) + ) + } + await tx.insert(permissionGroup).values(newGroup) + }) logger.info('Created permission group', { permissionGroupId: newGroup.id, @@ -211,6 +215,7 @@ export async function POST(req: Request) { actorEmail: session.user.email ?? undefined, resourceName: name, description: `Created permission group "${name}"`, + metadata: { organizationId, autoAddNewMembers: autoAddNewMembers || false }, request: req, }) diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index 597634aeb9d..d05514a8837 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -38,6 +38,7 @@ type ScheduleRow = { timezone: string | null sourceType: string | null sourceWorkspaceId: string | null + jobTitle: string | null } async function fetchAndAuthorize( @@ -55,6 +56,7 @@ async function fetchAndAuthorize( timezone: workflowSchedule.timezone, sourceType: workflowSchedule.sourceType, sourceWorkspaceId: workflowSchedule.sourceWorkspaceId, + jobTitle: workflowSchedule.jobTitle, }) .from(workflowSchedule) .where(and(eq(workflowSchedule.id, scheduleId), isNull(workflowSchedule.archivedAt))) @@ -144,13 +146,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ recordAudit({ workspaceId, actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, action: AuditAction.SCHEDULE_UPDATED, resourceType: AuditResourceType.SCHEDULE, resourceId: scheduleId, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - description: `Disabled schedule ${scheduleId}`, - metadata: {}, + resourceName: schedule.jobTitle ?? undefined, + description: `Disabled schedule "${schedule.jobTitle ?? scheduleId}"`, + metadata: { + operation: 'disable', + sourceType: schedule.sourceType, + previousStatus: schedule.status, + }, request, }) @@ -204,13 +211,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ recordAudit({ workspaceId, actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, action: AuditAction.SCHEDULE_UPDATED, resourceType: AuditResourceType.SCHEDULE, resourceId: scheduleId, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - description: `Updated job schedule ${scheduleId}`, - metadata: {}, + resourceName: schedule.jobTitle ?? undefined, + description: `Updated job schedule "${schedule.jobTitle ?? scheduleId}"`, + metadata: { + operation: 'update', + updatedFields: Object.keys(setFields).filter((k) => k !== 'updatedAt'), + }, request, }) @@ -246,13 +257,19 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ recordAudit({ workspaceId, actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, action: AuditAction.SCHEDULE_UPDATED, resourceType: AuditResourceType.SCHEDULE, resourceId: scheduleId, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - description: `Reactivated schedule ${scheduleId}`, - metadata: { cronExpression: schedule.cronExpression, timezone: schedule.timezone }, + resourceName: schedule.jobTitle ?? undefined, + description: `Reactivated schedule "${schedule.jobTitle ?? scheduleId}"`, + metadata: { + operation: 'reactivate', + sourceType: schedule.sourceType, + cronExpression: schedule.cronExpression, + timezone: schedule.timezone, + }, request, }) @@ -289,13 +306,18 @@ export async function DELETE( recordAudit({ workspaceId, actorId: session.user.id, - action: AuditAction.SCHEDULE_UPDATED, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.SCHEDULE_DELETED, resourceType: AuditResourceType.SCHEDULE, resourceId: scheduleId, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} ${scheduleId}`, - metadata: {}, + resourceName: schedule.jobTitle ?? undefined, + description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} "${schedule.jobTitle ?? scheduleId}"`, + metadata: { + sourceType: schedule.sourceType, + cronExpression: schedule.cronExpression, + timezone: schedule.timezone, + }, request, }) diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts index da291cdcccc..a2f14f109a8 100644 --- a/apps/sim/app/api/schedules/route.ts +++ b/apps/sim/app/api/schedules/route.ts @@ -3,6 +3,7 @@ import { workflow, workflowDeploymentVersion, workflowSchedule } from '@sim/db/s import { createLogger } from '@sim/logger' import { and, eq, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { generateId } from '@/lib/core/utils/uuid' @@ -279,6 +280,25 @@ export async function POST(req: NextRequest) { lifecycle, }) + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.SCHEDULE_CREATED, + resourceType: AuditResourceType.SCHEDULE, + resourceId: id, + resourceName: title.trim(), + description: `Created job schedule "${title.trim()}"`, + metadata: { + cronExpression, + timezone, + lifecycle, + maxRuns: maxRuns ?? null, + }, + request: req, + }) + captureServerEvent( session.user.id, 'scheduled_task_created', diff --git a/apps/sim/app/api/settings/allowed-providers/route.ts b/apps/sim/app/api/settings/allowed-providers/route.ts new file mode 100644 index 00000000000..2880c9eca08 --- /dev/null +++ b/apps/sim/app/api/settings/allowed-providers/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { getBlacklistedProvidersFromEnv } from '@/lib/core/config/feature-flags' + +export async function GET() { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + return NextResponse.json({ + blacklistedProviders: getBlacklistedProvidersFromEnv(), + }) +} diff --git a/apps/sim/app/api/skills/route.ts b/apps/sim/app/api/skills/route.ts index 41173c13188..f1db74a3cce 100644 --- a/apps/sim/app/api/skills/route.ts +++ b/apps/sim/app/api/skills/route.ts @@ -103,11 +103,14 @@ export async function POST(req: NextRequest) { recordAudit({ workspaceId, actorId: userId, + actorName: authResult.userName ?? undefined, + actorEmail: authResult.userEmail ?? undefined, action: AuditAction.SKILL_CREATED, resourceType: AuditResourceType.SKILL, resourceId: skill.id, resourceName: skill.name, description: `Created/updated skill "${skill.name}"`, + metadata: { source }, }) captureServerEvent( userId, @@ -185,10 +188,13 @@ export async function DELETE(request: NextRequest) { recordAudit({ workspaceId, actorId: authResult.userId, + actorName: authResult.userName ?? undefined, + actorEmail: authResult.userEmail ?? undefined, action: AuditAction.SKILL_DELETED, resourceType: AuditResourceType.SKILL, resourceId: skillId, description: `Deleted skill`, + metadata: { source }, }) captureServerEvent( diff --git a/apps/sim/app/api/table/[tableId]/restore/route.ts b/apps/sim/app/api/table/[tableId]/restore/route.ts index 9175de0b661..fca864c8753 100644 --- a/apps/sim/app/api/table/[tableId]/restore/route.ts +++ b/apps/sim/app/api/table/[tableId]/restore/route.ts @@ -45,6 +45,10 @@ export async function POST( resourceId: tableId, resourceName: table.name, description: `Restored table "${table.name}"`, + metadata: { + tableName: table.name, + workspaceId: table.workspaceId, + }, request, }) diff --git a/apps/sim/app/api/templates/[id]/route.ts b/apps/sim/app/api/templates/[id]/route.ts index 260b64f582b..82c73fffa0f 100644 --- a/apps/sim/app/api/templates/[id]/route.ts +++ b/apps/sim/app/api/templates/[id]/route.ts @@ -251,6 +251,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ resourceId: id, resourceName: name ?? template.name, description: `Updated template "${name ?? template.name}"`, + metadata: { + templateName: name ?? template.name, + updatedFields: Object.keys(validationResult.data).filter( + (k) => validationResult.data[k as keyof typeof validationResult.data] !== undefined + ), + statusChange: status !== undefined ? { from: template.status, to: status } : undefined, + stateUpdated: updateState || false, + workflowId: template.workflowId || undefined, + }, request, }) @@ -317,6 +326,13 @@ export async function DELETE( resourceId: id, resourceName: template.name, description: `Deleted template "${template.name}"`, + metadata: { + templateName: template.name, + workflowId: template.workflowId || undefined, + creatorId: template.creatorId || undefined, + status: template.status, + tags: template.tags, + }, request, }) diff --git a/apps/sim/app/api/templates/route.ts b/apps/sim/app/api/templates/route.ts index c6424865c82..0a9f9d02b72 100644 --- a/apps/sim/app/api/templates/route.ts +++ b/apps/sim/app/api/templates/route.ts @@ -346,6 +346,14 @@ export async function POST(request: NextRequest) { resourceId: templateId, resourceName: data.name, description: `Created template "${data.name}"`, + metadata: { + templateName: data.name, + workflowId: data.workflowId, + creatorId: data.creatorId, + tags: data.tags, + tagline: data.details?.tagline || undefined, + status: 'pending', + }, request, }) diff --git a/apps/sim/app/api/tools/custom/route.ts b/apps/sim/app/api/tools/custom/route.ts index 7d45353e609..426da0273ce 100644 --- a/apps/sim/app/api/tools/custom/route.ts +++ b/apps/sim/app/api/tools/custom/route.ts @@ -183,11 +183,14 @@ export async function POST(req: NextRequest) { recordAudit({ workspaceId, actorId: userId, + actorName: authResult.userName ?? undefined, + actorEmail: authResult.userEmail ?? undefined, action: AuditAction.CUSTOM_TOOL_CREATED, resourceType: AuditResourceType.CUSTOM_TOOL, resourceId: tool.id, resourceName: tool.title, description: `Created/updated custom tool "${tool.title}"`, + metadata: { source }, }) } @@ -304,10 +307,14 @@ export async function DELETE(request: NextRequest) { recordAudit({ workspaceId: tool.workspaceId || undefined, actorId: userId, + actorName: authResult.userName ?? undefined, + actorEmail: authResult.userEmail ?? undefined, action: AuditAction.CUSTOM_TOOL_DELETED, resourceType: AuditResourceType.CUSTOM_TOOL, resourceId: toolId, - description: `Deleted custom tool`, + resourceName: tool.title, + description: `Deleted custom tool "${tool.title}"`, + metadata: { source }, }) logger.info(`[${requestId}] Deleted tool: ${toolId}`) diff --git a/apps/sim/app/api/v1/admin/audit-logs/route.ts b/apps/sim/app/api/v1/admin/audit-logs/route.ts index 895ac1ff3e2..f97c755da33 100644 --- a/apps/sim/app/api/v1/admin/audit-logs/route.ts +++ b/apps/sim/app/api/v1/admin/audit-logs/route.ts @@ -21,7 +21,7 @@ import { db } from '@sim/db' import { auditLog } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, count, desc, eq, gte, lte, type SQL } from 'drizzle-orm' +import { and, count, desc } from 'drizzle-orm' import { withAdminAuth } from '@/app/api/v1/admin/middleware' import { badRequestResponse, @@ -34,6 +34,7 @@ import { parsePaginationParams, toAdminAuditLog, } from '@/app/api/v1/admin/types' +import { buildFilterConditions } from '@/app/api/v1/audit-logs/query' const logger = createLogger('AdminAuditLogsAPI') @@ -41,33 +42,27 @@ export const GET = withAdminAuth(async (request) => { const url = new URL(request.url) const { limit, offset } = parsePaginationParams(url) - const actionFilter = url.searchParams.get('action') - const resourceTypeFilter = url.searchParams.get('resourceType') - const resourceIdFilter = url.searchParams.get('resourceId') - const workspaceIdFilter = url.searchParams.get('workspaceId') - const actorIdFilter = url.searchParams.get('actorId') - const actorEmailFilter = url.searchParams.get('actorEmail') - const startDateFilter = url.searchParams.get('startDate') - const endDateFilter = url.searchParams.get('endDate') + const startDate = url.searchParams.get('startDate') || undefined + const endDate = url.searchParams.get('endDate') || undefined - if (startDateFilter && Number.isNaN(Date.parse(startDateFilter))) { + if (startDate && Number.isNaN(Date.parse(startDate))) { return badRequestResponse('Invalid startDate format. Use ISO 8601.') } - if (endDateFilter && Number.isNaN(Date.parse(endDateFilter))) { + if (endDate && Number.isNaN(Date.parse(endDate))) { return badRequestResponse('Invalid endDate format. Use ISO 8601.') } try { - const conditions: SQL[] = [] - - if (actionFilter) conditions.push(eq(auditLog.action, actionFilter)) - if (resourceTypeFilter) conditions.push(eq(auditLog.resourceType, resourceTypeFilter)) - if (resourceIdFilter) conditions.push(eq(auditLog.resourceId, resourceIdFilter)) - if (workspaceIdFilter) conditions.push(eq(auditLog.workspaceId, workspaceIdFilter)) - if (actorIdFilter) conditions.push(eq(auditLog.actorId, actorIdFilter)) - if (actorEmailFilter) conditions.push(eq(auditLog.actorEmail, actorEmailFilter)) - if (startDateFilter) conditions.push(gte(auditLog.createdAt, new Date(startDateFilter))) - if (endDateFilter) conditions.push(lte(auditLog.createdAt, new Date(endDateFilter))) + const conditions = buildFilterConditions({ + action: url.searchParams.get('action') || undefined, + resourceType: url.searchParams.get('resourceType') || undefined, + resourceId: url.searchParams.get('resourceId') || undefined, + workspaceId: url.searchParams.get('workspaceId') || undefined, + actorId: url.searchParams.get('actorId') || undefined, + actorEmail: url.searchParams.get('actorEmail') || undefined, + startDate, + endDate, + }) const whereClause = conditions.length > 0 ? and(...conditions) : undefined diff --git a/apps/sim/app/api/v1/audit-logs/query.ts b/apps/sim/app/api/v1/audit-logs/query.ts new file mode 100644 index 00000000000..14e24c65427 --- /dev/null +++ b/apps/sim/app/api/v1/audit-logs/query.ts @@ -0,0 +1,146 @@ +import { db } from '@sim/db' +import { auditLog, workspace } from '@sim/db/schema' +import type { InferSelectModel } from 'drizzle-orm' +import { and, desc, eq, gte, ilike, inArray, lt, lte, or, type SQL, sql } from 'drizzle-orm' + +type DbAuditLog = InferSelectModel + +interface CursorData { + createdAt: string + id: string +} + +function encodeCursor(data: CursorData): string { + return Buffer.from(JSON.stringify(data)).toString('base64') +} + +function decodeCursor(cursor: string): CursorData | null { + try { + return JSON.parse(Buffer.from(cursor, 'base64').toString()) + } catch { + return null + } +} + +export interface AuditLogFilterParams { + action?: string + resourceType?: string + resourceId?: string + workspaceId?: string + actorId?: string + actorEmail?: string + search?: string + startDate?: string + endDate?: string +} + +export function buildFilterConditions(params: AuditLogFilterParams): SQL[] { + const conditions: SQL[] = [] + + if (params.action) conditions.push(eq(auditLog.action, params.action)) + if (params.resourceType) conditions.push(eq(auditLog.resourceType, params.resourceType)) + if (params.resourceId) conditions.push(eq(auditLog.resourceId, params.resourceId)) + if (params.workspaceId) conditions.push(eq(auditLog.workspaceId, params.workspaceId)) + if (params.actorId) conditions.push(eq(auditLog.actorId, params.actorId)) + if (params.actorEmail) conditions.push(eq(auditLog.actorEmail, params.actorEmail)) + + if (params.search) { + const escaped = params.search.replace(/[%_\\]/g, '\\$&') + const searchTerm = `%${escaped}%` + conditions.push( + or( + ilike(auditLog.action, searchTerm), + ilike(auditLog.actorEmail, searchTerm), + ilike(auditLog.actorName, searchTerm), + ilike(auditLog.resourceName, searchTerm), + ilike(auditLog.description, searchTerm) + )! + ) + } + + if (params.startDate) conditions.push(gte(auditLog.createdAt, new Date(params.startDate))) + if (params.endDate) conditions.push(lte(auditLog.createdAt, new Date(params.endDate))) + + return conditions +} + +export async function buildOrgScopeCondition( + orgMemberIds: string[], + includeDeparted: boolean +): Promise> { + if (orgMemberIds.length === 0) { + return sql`1 = 0` + } + + if (!includeDeparted) { + return inArray(auditLog.actorId, orgMemberIds) + } + + const orgWorkspaces = await db + .select({ id: workspace.id }) + .from(workspace) + .where(inArray(workspace.ownerId, orgMemberIds)) + + const orgWorkspaceIds = orgWorkspaces.map((w) => w.id) + + if (orgWorkspaceIds.length > 0) { + return or( + inArray(auditLog.actorId, orgMemberIds), + inArray(auditLog.workspaceId, orgWorkspaceIds) + )! + } + + return inArray(auditLog.actorId, orgMemberIds) +} + +function buildCursorCondition(cursor: string): SQL | null { + const cursorData = decodeCursor(cursor) + if (!cursorData?.createdAt || !cursorData.id) return null + + const cursorDate = new Date(cursorData.createdAt) + if (Number.isNaN(cursorDate.getTime())) return null + + return or( + lt(auditLog.createdAt, cursorDate), + and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id)) + )! +} + +interface CursorPaginatedResult { + data: DbAuditLog[] + nextCursor?: string +} + +export async function queryAuditLogs( + conditions: SQL[], + limit: number, + cursor?: string +): Promise { + const allConditions = [...conditions] + + if (cursor) { + const cursorCondition = buildCursorCondition(cursor) + if (cursorCondition) allConditions.push(cursorCondition) + } + + const rows = await db + .select() + .from(auditLog) + .where(allConditions.length > 0 ? and(...allConditions) : undefined) + .orderBy(desc(auditLog.createdAt), desc(auditLog.id)) + .limit(limit + 1) + + const hasMore = rows.length > limit + const data = rows.slice(0, limit) + + let nextCursor: string | undefined + if (hasMore && data.length > 0) { + const last = data[data.length - 1] + nextCursor = encodeCursor({ + createdAt: last.createdAt.toISOString(), + id: last.id, + }) + } + + return { data, nextCursor } +} diff --git a/apps/sim/app/api/v1/audit-logs/route.ts b/apps/sim/app/api/v1/audit-logs/route.ts index 5a090391da4..046680bde44 100644 --- a/apps/sim/app/api/v1/audit-logs/route.ts +++ b/apps/sim/app/api/v1/audit-logs/route.ts @@ -19,15 +19,17 @@ * Response: { data: AuditLogEntry[], nextCursor?: string, limits: UserLimits } */ -import { db } from '@sim/db' -import { auditLog, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, desc, eq, gte, inArray, lt, lte, or, type SQL } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { generateId } from '@/lib/core/utils/uuid' import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth' import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format' +import { + buildFilterConditions, + buildOrgScopeCondition, + queryAuditLogs, +} from '@/app/api/v1/audit-logs/query' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' @@ -57,23 +59,6 @@ const QueryParamsSchema = z.object({ cursor: z.string().optional(), }) -interface CursorData { - createdAt: string - id: string -} - -function encodeCursor(data: CursorData): string { - return Buffer.from(JSON.stringify(data)).toString('base64') -} - -function decodeCursor(cursor: string): CursorData | null { - try { - return JSON.parse(Buffer.from(cursor, 'base64').toString()) - } catch { - return null - } -} - export async function GET(request: NextRequest) { const requestId = generateId().slice(0, 8) @@ -112,71 +97,22 @@ export async function GET(request: NextRequest) { ) } - let scopeCondition: SQL - - if (params.includeDeparted) { - const orgWorkspaces = await db - .select({ id: workspace.id }) - .from(workspace) - .where(inArray(workspace.ownerId, orgMemberIds)) - - const orgWorkspaceIds = orgWorkspaces.map((w) => w.id) - - if (orgWorkspaceIds.length > 0) { - scopeCondition = or( - inArray(auditLog.actorId, orgMemberIds), - inArray(auditLog.workspaceId, orgWorkspaceIds) - )! - } else { - scopeCondition = inArray(auditLog.actorId, orgMemberIds) - } - } else { - scopeCondition = inArray(auditLog.actorId, orgMemberIds) - } - - const conditions: SQL[] = [scopeCondition] - - if (params.action) conditions.push(eq(auditLog.action, params.action)) - if (params.resourceType) conditions.push(eq(auditLog.resourceType, params.resourceType)) - if (params.resourceId) conditions.push(eq(auditLog.resourceId, params.resourceId)) - if (params.workspaceId) conditions.push(eq(auditLog.workspaceId, params.workspaceId)) - if (params.actorId) conditions.push(eq(auditLog.actorId, params.actorId)) - if (params.startDate) conditions.push(gte(auditLog.createdAt, new Date(params.startDate))) - if (params.endDate) conditions.push(lte(auditLog.createdAt, new Date(params.endDate))) - - if (params.cursor) { - const cursorData = decodeCursor(params.cursor) - if (cursorData?.createdAt && cursorData.id) { - const cursorDate = new Date(cursorData.createdAt) - if (!Number.isNaN(cursorDate.getTime())) { - conditions.push( - or( - lt(auditLog.createdAt, cursorDate), - and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id)) - )! - ) - } - } - } - - const rows = await db - .select() - .from(auditLog) - .where(and(...conditions)) - .orderBy(desc(auditLog.createdAt), desc(auditLog.id)) - .limit(params.limit + 1) - - const hasMore = rows.length > params.limit - const data = rows.slice(0, params.limit) - - let nextCursor: string | undefined - if (hasMore && data.length > 0) { - const last = data[data.length - 1] - nextCursor = encodeCursor({ - createdAt: last.createdAt.toISOString(), - id: last.id, - }) - } + const scopeCondition = await buildOrgScopeCondition(orgMemberIds, params.includeDeparted) + const filterConditions = buildFilterConditions({ + action: params.action, + resourceType: params.resourceType, + resourceId: params.resourceId, + workspaceId: params.workspaceId, + actorId: params.actorId, + startDate: params.startDate, + endDate: params.endDate, + }) + + const { data, nextCursor } = await queryAuditLogs( + [scopeCondition, ...filterConditions], + params.limit, + params.cursor + ) const formattedLogs = data.map(formatAuditLogEntry) diff --git a/apps/sim/app/api/v1/files/[fileId]/route.ts b/apps/sim/app/api/v1/files/[fileId]/route.ts index 7007053681b..b3d3db8ceb8 100644 --- a/apps/sim/app/api/v1/files/[fileId]/route.ts +++ b/apps/sim/app/api/v1/files/[fileId]/route.ts @@ -142,6 +142,7 @@ export async function DELETE(request: NextRequest, { params }: FileRouteParams) resourceId: fileId, resourceName: fileRecord.name, description: `Archived file "${fileRecord.name}" via API`, + metadata: { fileSize: fileRecord.size, fileType: fileRecord.type }, request, }) diff --git a/apps/sim/app/api/v1/files/route.ts b/apps/sim/app/api/v1/files/route.ts index 8c344c1575d..2781fe164a2 100644 --- a/apps/sim/app/api/v1/files/route.ts +++ b/apps/sim/app/api/v1/files/route.ts @@ -155,6 +155,7 @@ export async function POST(request: NextRequest) { resourceId: userFile.id, resourceName: file.name, description: `Uploaded file "${file.name}" via API`, + metadata: { fileSize: file.size, fileType: file.type || 'application/octet-stream' }, request, }) diff --git a/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts index b69721329a4..22d40d979f1 100644 --- a/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts +++ b/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts @@ -167,6 +167,7 @@ export async function DELETE(request: NextRequest, { params }: DocumentDetailRou resourceId: documentId, resourceName: docs[0].filename, description: `Deleted document "${docs[0].filename}" from knowledge base via API`, + metadata: { knowledgeBaseId }, request, }) diff --git a/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts b/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts index 7310a4eca98..6eb61e22614 100644 --- a/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts @@ -207,6 +207,7 @@ export async function POST(request: NextRequest, { params }: DocumentsRouteParam resourceId: newDocument.id, resourceName: file.name, description: `Uploaded document "${file.name}" to knowledge base via API`, + metadata: { knowledgeBaseId, fileSize: file.size, mimeType: contentType }, request, }) diff --git a/apps/sim/app/api/v1/knowledge/[id]/route.ts b/apps/sim/app/api/v1/knowledge/[id]/route.ts index 0b7012c8770..b6b5ed3c8ac 100644 --- a/apps/sim/app/api/v1/knowledge/[id]/route.ts +++ b/apps/sim/app/api/v1/knowledge/[id]/route.ts @@ -111,6 +111,7 @@ export async function PUT(request: NextRequest, { params }: KnowledgeRouteParams resourceId: id, resourceName: updatedKb.name, description: `Updated knowledge base "${updatedKb.name}" via API`, + metadata: { updatedFields: Object.keys(updates) }, request, }) diff --git a/apps/sim/app/api/v1/knowledge/route.ts b/apps/sim/app/api/v1/knowledge/route.ts index 9d45e677bd3..61741d3c59e 100644 --- a/apps/sim/app/api/v1/knowledge/route.ts +++ b/apps/sim/app/api/v1/knowledge/route.ts @@ -106,6 +106,7 @@ export async function POST(request: NextRequest) { resourceId: kb.id, resourceName: kb.name, description: `Created knowledge base "${kb.name}" via API`, + metadata: { chunkingConfig }, request, }) diff --git a/apps/sim/app/api/v1/tables/route.ts b/apps/sim/app/api/v1/tables/route.ts index 09ff717f9cd..d0c0ad3e64d 100644 --- a/apps/sim/app/api/v1/tables/route.ts +++ b/apps/sim/app/api/v1/tables/route.ts @@ -206,6 +206,7 @@ export async function POST(request: NextRequest) { resourceId: table.id, resourceName: table.name, description: `Created table "${table.name}" via API`, + metadata: { columnCount: params.schema.columns.length }, request, }) diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts index 24d93fc0609..e146c939507 100644 --- a/apps/sim/app/api/webhooks/[id]/route.ts +++ b/apps/sim/app/api/webhooks/[id]/route.ts @@ -270,8 +270,14 @@ export async function DELETE( resourceType: AuditResourceType.WEBHOOK, resourceId: id, resourceName: foundWebhook.provider || 'generic', - description: 'Deleted webhook', - metadata: { workflowId: webhookData.workflow.id }, + description: `Deleted ${foundWebhook.provider || 'generic'} webhook`, + metadata: { + provider: foundWebhook.provider || 'generic', + workflowId: webhookData.workflow.id, + webhookPath: foundWebhook.path || undefined, + blockId: foundWebhook.blockId || undefined, + credentialSetId: credentialSetId || undefined, + }, request, }) diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index c6ef9e992e1..0c7174f4294 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -687,7 +687,12 @@ export async function POST(request: NextRequest) { resourceId: savedWebhook.id, resourceName: provider || 'generic', description: `Created ${provider || 'generic'} webhook`, - metadata: { provider, workflowId }, + metadata: { + provider: provider || 'generic', + workflowId, + webhookPath: finalPath, + blockId: blockId || undefined, + }, request, }) diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts index a209db29eb4..a2fb4fe4ba4 100644 --- a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts @@ -127,6 +127,9 @@ export async function POST( actorEmail: session!.user.email ?? undefined, resourceName: workflowRecord?.name ?? undefined, description: `Reverted workflow to deployment version ${version}`, + metadata: { + targetVersion: version, + }, request, }) diff --git a/apps/sim/app/api/workflows/[id]/duplicate/route.ts b/apps/sim/app/api/workflows/[id]/duplicate/route.ts index 63c230f686b..0af8a82bae0 100644 --- a/apps/sim/app/api/workflows/[id]/duplicate/route.ts +++ b/apps/sim/app/api/workflows/[id]/duplicate/route.ts @@ -87,7 +87,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: resourceId: result.id, resourceName: result.name, description: `Duplicated workflow from ${sourceWorkflowId}`, - metadata: { sourceWorkflowId }, + metadata: { + sourceWorkflowId, + newWorkflowId: result.id, + folderId: folderId || undefined, + }, request: req, }) diff --git a/apps/sim/app/api/workflows/[id]/restore/route.ts b/apps/sim/app/api/workflows/[id]/restore/route.ts index a9d6b6ba1a5..c0b4d3d535f 100644 --- a/apps/sim/app/api/workflows/[id]/restore/route.ts +++ b/apps/sim/app/api/workflows/[id]/restore/route.ts @@ -56,6 +56,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ resourceId: workflowId, resourceName: workflowData.name, description: `Restored workflow "${workflowData.name}"`, + metadata: { + workflowName: workflowData.name, + workspaceId: workflowData.workspaceId || undefined, + }, request, }) diff --git a/apps/sim/app/api/workflows/[id]/variables/route.ts b/apps/sim/app/api/workflows/[id]/variables/route.ts index 1b4cd8ab3b9..064669c9b8f 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.ts @@ -90,7 +90,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: resourceId: workflowId, resourceName: workflowData.name ?? undefined, description: `Updated workflow variables`, - metadata: { variableCount: Object.keys(variables).length }, + metadata: { + variableCount: Object.keys(variables).length, + variableNames: Object.values(variables).map((v) => v.name), + workflowName: workflowData.name ?? undefined, + }, request: req, }) diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 3615afd0890..f96bd6d352f 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -296,7 +296,14 @@ export async function POST(req: NextRequest) { resourceId: workflowId, resourceName: name, description: `Created workflow "${name}"`, - metadata: { name }, + metadata: { + name, + description: description || undefined, + color, + workspaceId, + folderId: folderId || undefined, + sortOrder, + }, request: req, }) diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts index 42711f1fa8c..3345888a6f7 100644 --- a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts @@ -97,7 +97,12 @@ export async function PUT( actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, resourceName: name, - description: `Updated workspace API key: ${name}`, + description: `Renamed workspace API key from "${existingKey[0].name}" to "${name}"`, + metadata: { + keyType: 'workspace', + previousName: existingKey[0].name, + newName: name, + }, request, }) @@ -163,7 +168,11 @@ export async function DELETE( actorEmail: session.user.email ?? undefined, resourceName: deletedKey.name, description: `Revoked workspace API key: ${deletedKey.name}`, - metadata: { lastUsed: deletedKey.lastUsed?.toISOString() ?? null }, + metadata: { + keyType: 'workspace', + keyName: deletedKey.name, + lastUsed: deletedKey.lastUsed?.toISOString() ?? null, + }, request, }) diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts index a6a15bb52f2..4c156d06f94 100644 --- a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts @@ -182,7 +182,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ resourceId: newKey.id, resourceName: name, description: `Created API key "${name}"`, - metadata: { keyName: name }, + metadata: { keyName: name, keyType: 'workspace', source: source ?? 'settings' }, request, }) @@ -257,8 +257,8 @@ export async function DELETE( actorEmail: session?.user?.email, action: AuditAction.API_KEY_REVOKED, resourceType: AuditResourceType.API_KEY, - description: `Revoked ${deletedCount} API key(s)`, - metadata: { keyIds: keys, deletedCount }, + description: `Revoked ${deletedCount} workspace API key(s)`, + metadata: { keyIds: keys, deletedCount, keyType: 'workspace' }, request, }) diff --git a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts index 65f177b1c55..5ccda1fae77 100644 --- a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts @@ -172,6 +172,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ logger.info(`[${requestId}] Updated BYOK key for ${providerId} in workspace ${workspaceId}`) + recordAudit({ + workspaceId, + actorId: userId, + actorName: session?.user?.name, + actorEmail: session?.user?.email, + action: AuditAction.BYOK_KEY_UPDATED, + resourceType: AuditResourceType.BYOK_KEY, + resourceId: existingKey[0].id, + resourceName: providerId, + description: `Updated BYOK key for ${providerId}`, + metadata: { providerId }, + request, + }) + return NextResponse.json({ success: true, key: { diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts index 2e118b628d7..67b1eddeb7a 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts @@ -140,8 +140,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ action: AuditAction.ENVIRONMENT_UPDATED, resourceType: AuditResourceType.ENVIRONMENT, resourceId: workspaceId, - description: `Updated environment variables`, - metadata: { variableCount: Object.keys(variables).length }, + description: `Updated ${Object.keys(variables).length} workspace environment variable(s)`, + metadata: { + variableCount: Object.keys(variables).length, + updatedKeys: Object.keys(variables), + totalKeysAfterUpdate: Object.keys(merged).length, + }, request, }) @@ -217,6 +221,22 @@ export async function DELETE( actingUserId: userId, }) + recordAudit({ + workspaceId, + actorId: userId, + actorName: session?.user?.name, + actorEmail: session?.user?.email, + action: AuditAction.ENVIRONMENT_DELETED, + resourceType: AuditResourceType.ENVIRONMENT, + resourceId: workspaceId, + description: `Removed ${keys.length} workspace environment variable(s)`, + metadata: { + removedKeys: keys, + remainingKeysCount: Object.keys(current).length, + }, + request, + }) + return NextResponse.json({ success: true }) } catch (error: any) { logger.error(`[${requestId}] Workspace env DELETE error`, error) diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts index 24b5eb56cf0..179efc41d3f 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts @@ -69,7 +69,9 @@ export async function PUT( action: AuditAction.FILE_UPDATED, resourceType: AuditResourceType.FILE, resourceId: fileId, + resourceName: updatedFile.name, description: `Updated content of file "${updatedFile.name}"`, + metadata: { contentSize: buffer.length }, request, }) diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts index c440618863e..34cacc6808d 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts @@ -58,6 +58,7 @@ export async function PATCH( action: AuditAction.FILE_UPDATED, resourceType: AuditResourceType.FILE, resourceId: fileId, + resourceName: updatedFile.name, description: `Renamed file to "${updatedFile.name}"`, request, }) diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts index 5c887442796..41bdf82569f 100644 --- a/apps/sim/app/api/workspaces/[id]/files/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/route.ts @@ -134,6 +134,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ resourceId: userFile.id, resourceName: fileName, description: `Uploaded file "${fileName}"`, + metadata: { fileSize: rawFile.size, fileType: rawFile.type || 'application/octet-stream' }, request, }) diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts index 08d3f5802d2..ae5ae96c3e6 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts @@ -262,6 +262,14 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, description: `Updated ${subscription.notificationType} notification subscription`, + metadata: { + notificationType: subscription.notificationType, + updatedFields: Object.keys(data).filter( + (k) => (data as Record)[k] !== undefined + ), + ...(data.active !== undefined && { active: data.active }), + ...(data.alertConfig !== undefined && { alertRule: data.alertConfig?.rule ?? null }), + }, request, }) @@ -340,6 +348,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { actorEmail: session.user.email ?? undefined, resourceName: deletedSubscription.notificationType, description: `Deleted ${deletedSubscription.notificationType} notification subscription`, + metadata: { + notificationType: deletedSubscription.notificationType, + }, request, }) diff --git a/apps/sim/app/api/workspaces/[id]/notifications/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/route.ts index 1a18f8d2386..3ad7532f8e8 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/route.ts @@ -278,6 +278,17 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, description: `Created ${data.notificationType} notification subscription`, + metadata: { + notificationType: data.notificationType, + allWorkflows: data.allWorkflows, + workflowCount: data.workflowIds.length, + levelFilter: data.levelFilter, + alertRule: data.alertConfig?.rule ?? null, + ...(data.notificationType === 'email' && { + recipientCount: data.emailRecipients?.length ?? 0, + }), + ...(data.notificationType === 'slack' && { channelName: data.slackConfig?.channelName }), + }, request, }) diff --git a/apps/sim/app/api/workspaces/[id]/permissions/route.ts b/apps/sim/app/api/workspaces/[id]/permissions/route.ts index f31bce34ba6..e7ee5385597 100644 --- a/apps/sim/app/api/workspaces/[id]/permissions/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permissions/route.ts @@ -202,19 +202,15 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< action: AuditAction.MEMBER_ROLE_CHANGED, resourceType: AuditResourceType.WORKSPACE, resourceId: workspaceId, + resourceName: permLookup.get(update.userId)?.email ?? update.userId, actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, - description: `Changed permissions for user ${update.userId} to ${update.permissions}`, + description: `Changed permissions for ${permLookup.get(update.userId)?.email ?? update.userId} from ${permLookup.get(update.userId)?.permission ?? 'none'} to ${update.permissions}`, metadata: { targetUserId: update.userId, targetEmail: permLookup.get(update.userId)?.email ?? undefined, - changes: [ - { - field: 'permissions', - from: permLookup.get(update.userId)?.permission ?? null, - to: update.permissions, - }, - ], + previousRole: permLookup.get(update.userId)?.permission ?? null, + newRole: update.permissions, }, request, }) diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts index 375e0879b8b..ca4e9408fcf 100644 --- a/apps/sim/app/api/workspaces/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/route.ts @@ -202,6 +202,37 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< .where(eq(workspace.id, workspaceId)) .then((rows) => rows[0]) + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.WORKSPACE_UPDATED, + resourceType: AuditResourceType.WORKSPACE, + resourceId: workspaceId, + resourceName: updatedWorkspace?.name ?? existingWorkspace.name, + description: `Updated workspace "${updatedWorkspace?.name ?? existingWorkspace.name}"`, + metadata: { + changes: { + ...(name !== undefined && { name: { from: existingWorkspace.name, to: name } }), + ...(color !== undefined && { color: { from: existingWorkspace.color, to: color } }), + ...(allowPersonalApiKeys !== undefined && { + allowPersonalApiKeys: { + from: existingWorkspace.allowPersonalApiKeys, + to: allowPersonalApiKeys, + }, + }), + ...(billedAccountUserId !== undefined && { + billedAccountUserId: { + from: existingWorkspace.billedAccountUserId, + to: billedAccountUserId, + }, + }), + }, + }, + request, + }) + return NextResponse.json({ workspace: { ...updatedWorkspace, diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts index 602b60e88cd..d76322d4e5f 100644 --- a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts +++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts @@ -189,7 +189,13 @@ export async function GET( actorEmail: session.user.email ?? undefined, resourceName: workspaceDetails.name, description: `Accepted workspace invitation to "${workspaceDetails.name}"`, - metadata: { targetEmail: invitation.email }, + metadata: { + targetEmail: invitation.email, + workspaceName: workspaceDetails.name, + assignedPermission: invitation.permissions || 'read', + invitationId: invitation.id, + inviterId: invitation.inviterId, + }, request: req, }) @@ -272,7 +278,11 @@ export async function DELETE( actorName: session.user.name ?? undefined, actorEmail: session.user.email ?? undefined, description: `Revoked workspace invitation for ${invitation.email}`, - metadata: { invitationId, targetEmail: invitation.email }, + metadata: { + invitationId, + targetEmail: invitation.email, + invitationStatus: invitation.status, + }, request: _request, }) @@ -360,6 +370,24 @@ export async function POST( ) } + recordAudit({ + workspaceId: invitation.workspaceId, + actorId: session.user.id, + action: AuditAction.INVITATION_RESENT, + resourceType: AuditResourceType.WORKSPACE, + resourceId: invitation.workspaceId, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + resourceName: ws.name, + description: `Resent workspace invitation to ${invitation.email}`, + metadata: { + invitationId, + targetEmail: invitation.email, + workspaceName: ws.name, + }, + request: _request, + }) + return NextResponse.json({ success: true }) } catch (error) { logger.error('Error resending workspace invitation:', error) diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts index 30c91acd22f..020e350dbb2 100644 --- a/apps/sim/app/api/workspaces/invitations/route.ts +++ b/apps/sim/app/api/workspaces/invitations/route.ts @@ -243,7 +243,12 @@ export async function POST(req: NextRequest) { resourceId: workspaceId, resourceName: email, description: `Invited ${email} as ${permission}`, - metadata: { targetEmail: email, targetRole: permission }, + metadata: { + targetEmail: email, + targetRole: permission, + workspaceName: workspaceDetails.name, + invitationId: invitationData.id, + }, request: req, }) diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts index ca918712946..e4a507c5a78 100644 --- a/apps/sim/app/api/workspaces/members/[id]/route.ts +++ b/apps/sim/app/api/workspaces/members/[id]/route.ts @@ -121,8 +121,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i action: AuditAction.MEMBER_REMOVED, resourceType: AuditResourceType.WORKSPACE, resourceId: workspaceId, - description: isSelf ? 'Left the workspace' : 'Removed a member from the workspace', - metadata: { removedUserId: userId, selfRemoval: isSelf }, + description: isSelf ? 'Left the workspace' : `Removed member ${userId} from the workspace`, + metadata: { + removedUserId: userId, + removedUserRole: userPermission.permissionType, + selfRemoval: isSelf, + }, request: req, }) diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index d64fd05f758..1fdc15f94df 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -118,7 +118,7 @@ export async function POST(req: Request) { resourceId: newWorkspace.id, resourceName: newWorkspace.name, description: `Created workspace "${newWorkspace.name}"`, - metadata: { name: newWorkspace.name }, + metadata: { name: newWorkspace.name, color: newWorkspace.color }, request: req, }) diff --git a/apps/sim/app/llms-full.txt/route.ts b/apps/sim/app/llms-full.txt/route.ts index 915a2354143..b198f56aa6f 100644 --- a/apps/sim/app/llms-full.txt/route.ts +++ b/apps/sim/app/llms-full.txt/route.ts @@ -1,20 +1,20 @@ import { getBaseUrl } from '@/lib/core/utils/urls' -export async function GET() { +export function GET() { const baseUrl = getBaseUrl() - const llmsFullContent = `# Sim — Build AI Agents & Run Your Agentic Workforce + const llmsFullContent = `# Sim — The AI Workspace | Build, Deploy & Manage AI Agents -> Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. +> Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work. ## Overview -Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over 100,000 builders use Sim — from startups to Fortune 500 companies. Teams connect their tools and data, build agents that execute real workflows across systems, and manage them with full observability. SOC2 compliant. +Sim is the AI workspace where teams create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. Over 100,000 builders use Sim — from startups to Fortune 500 companies. Teams connect their tools and data, build agents that automate real work across systems, and manage them with full observability. SOC2 compliant. ## Product Details - **Product Name**: Sim -- **Category**: AI Agent Platform / Agentic Workflow Orchestration +- **Category**: AI Workspace / AI Agent Builder - **Deployment**: Cloud (SaaS) and Self-hosted options - **Pricing**: Free tier, Pro ($25/month, 6K credits), Max ($100/month, 25K credits), Team plans available, Enterprise (custom) - **Compliance**: SOC2 Type II @@ -123,7 +123,7 @@ Built-in table creation and management: ### Frontend - Next.js 15 with App Router -- React Flow for canvas visualization +- React Flow for the visual builder - Tailwind CSS for styling - Zustand for state management @@ -143,7 +143,7 @@ Built-in table creation and management: 1. **Sign Up**: Create a free account at ${baseUrl} 2. **Create Workspace**: Set up your first workspace -3. **Build Workflow**: Drag blocks onto canvas and connect them +3. **Build Workflow**: Drag blocks onto the workflow builder and connect them 4. **Configure Blocks**: Set up LLM providers, tools, and integrations 5. **Test**: Run the workflow manually to verify 6. **Deploy**: Set up triggers for automated execution diff --git a/apps/sim/app/llms.txt/route.ts b/apps/sim/app/llms.txt/route.ts index 89fbc5a67f4..0e6f7c31873 100644 --- a/apps/sim/app/llms.txt/route.ts +++ b/apps/sim/app/llms.txt/route.ts @@ -5,9 +5,9 @@ export function GET() { const content = `# Sim -> Sim is the open-source platform to build AI agents and run your agentic workforce. Connect integrations and LLMs to deploy and orchestrate agentic workflows. +> Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work. -Sim lets teams create agents, workflows, knowledge bases, tables, and docs. It supports both product discovery pages and deeper technical documentation. +Sim lets teams create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. The workspace includes knowledge bases, tables, files, and full observability. ## Preferred URLs @@ -25,8 +25,8 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. It s ## Key Concepts -- **Workspace**: Container for workflows, data sources, and executions -- **Workflow**: Directed graph of blocks defining an agentic process +- **Workspace**: The AI workspace — container for agents, workflows, data sources, and executions +- **Workflow**: Visual builder — directed graph of blocks defining agent logic - **Block**: Individual step such as an LLM call, tool call, HTTP request, or code execution - **Trigger**: Event or schedule that initiates workflow execution - **Execution**: A single run of a workflow with logs and outputs @@ -34,8 +34,8 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. It s ## Capabilities +- AI workspace for teams - AI agent creation and deployment -- Agentic workflow orchestration - Integrations across business tools, databases, and communication platforms - Multi-model LLM orchestration - Knowledge bases and retrieval-augmented generation diff --git a/apps/sim/app/manifest.ts b/apps/sim/app/manifest.ts index 77c92d0c394..d66d2db1a17 100644 --- a/apps/sim/app/manifest.ts +++ b/apps/sim/app/manifest.ts @@ -5,10 +5,13 @@ export default function manifest(): MetadataRoute.Manifest { const brand = getBrandConfig() return { - name: brand.name === 'Sim' ? 'Sim — Build AI Agents & Run Your Agentic Workforce' : brand.name, + name: + brand.name === 'Sim' + ? 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents' + : brand.name, short_name: brand.name, description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.', start_url: '/', scope: '/', display: 'standalone', diff --git a/apps/sim/app/page.tsx b/apps/sim/app/page.tsx index c5e693a4cac..f746d2b3da6 100644 --- a/apps/sim/app/page.tsx +++ b/apps/sim/app/page.tsx @@ -9,12 +9,12 @@ const baseUrl = getBaseUrl() export const metadata: Metadata = { metadataBase: new URL(baseUrl), title: { - absolute: 'Sim — Build AI Agents & Run Your Agentic Workforce', + absolute: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents', }, description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.', keywords: - 'AI agents, agentic workforce, open-source AI agent platform, agentic workflows, LLM orchestration, AI automation, knowledge base, workflow builder, AI integrations, SOC2 compliant, enterprise AI', + 'AI workspace, AI agent builder, AI agent workflow builder, build AI agents, visual workflow builder, open-source AI agent platform, AI agents, agentic workflows, LLM orchestration, AI automation, knowledge base, workflow builder, AI integrations, SOC2 compliant, enterprise AI', authors: [{ name: 'Sim' }], creator: 'Sim', publisher: 'Sim', @@ -24,9 +24,9 @@ export const metadata: Metadata = { telephone: false, }, openGraph: { - title: 'Sim — Build AI Agents & Run Your Agentic Workforce', + title: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents', description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Join over 100,000 builders.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.', type: 'website', url: baseUrl, siteName: 'Sim', @@ -36,7 +36,7 @@ export const metadata: Metadata = { url: '/logo/426-240/primary/small.png', width: 2130, height: 1200, - alt: 'Sim — Build AI Agents & Run Your Agentic Workforce', + alt: 'Sim — The AI Workspace for Teams', type: 'image/png', }, ], @@ -45,12 +45,12 @@ export const metadata: Metadata = { card: 'summary_large_image', site: '@simdotai', creator: '@simdotai', - title: 'Sim — Build AI Agents & Run Your Agentic Workforce', + title: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents', description: - 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.', + 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.', images: { url: '/logo/426-240/primary/small.png', - alt: 'Sim — Build AI Agents & Run Your Agentic Workforce', + alt: 'Sim — The AI Workspace for Teams', }, }, alternates: { @@ -78,9 +78,9 @@ export const metadata: Metadata = { referrer: 'origin-when-cross-origin', other: { 'llm:content-type': - 'AI agent platform, agentic workforce, agentic workflows, LLM orchestration', + 'AI workspace, AI agent builder, AI agent platform, agentic workflows, LLM orchestration', 'llm:use-cases': - 'AI agents, agentic workforce, agentic workflows, knowledge bases, tables, document creation, email automation, Slack bots, data analysis, customer support, content generation', + 'build AI agents, AI workspace, visual workflow builder, natural language agent creation, knowledge bases, tables, document creation, email automation, Slack bots, data analysis, customer support, content generation', 'llm:integrations': 'OpenAI, Anthropic, Google AI, Mistral, xAI, Perplexity, Slack, Gmail, Discord, Notion, Airtable, Supabase', 'llm:pricing': diff --git a/apps/sim/app/robots.ts b/apps/sim/app/robots.ts index 710acc532c7..0a1a8929a33 100644 --- a/apps/sim/app/robots.ts +++ b/apps/sim/app/robots.ts @@ -119,6 +119,16 @@ export default function robots(): MetadataRoute.Robots { allow: '/', disallow: disallowedPaths, }, + { + userAgent: 'Grok-web-crawl', + allow: '/', + disallow: disallowedPaths, + }, + { + userAgent: 'DeepSeek-AI', + allow: '/', + disallow: disallowedPaths, + }, ], sitemap: `${baseUrl}/sitemap.xml`, host: baseUrl, diff --git a/apps/sim/app/sitemap.ts b/apps/sim/app/sitemap.ts index 0928460c001..32cf3f27be7 100644 --- a/apps/sim/app/sitemap.ts +++ b/apps/sim/app/sitemap.ts @@ -11,19 +11,27 @@ export default async function sitemap(): Promise { const integrationPages: MetadataRoute.Sitemap = integrations.map((integration) => ({ url: `${baseUrl}/integrations/${integration.slug}`, lastModified: now, + changeFrequency: 'monthly', + priority: 0.6, })) const modelHubPages: MetadataRoute.Sitemap = [ { url: `${baseUrl}/integrations`, lastModified: now, + changeFrequency: 'weekly', + priority: 0.8, }, { url: `${baseUrl}/models`, lastModified: now, + changeFrequency: 'weekly', + priority: 0.8, }, { url: `${baseUrl}/partners`, lastModified: now, + changeFrequency: 'monthly', + priority: 0.5, }, ] const providerPages: MetadataRoute.Sitemap = MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => ({ @@ -31,16 +39,22 @@ export default async function sitemap(): Promise { lastModified: new Date( Math.max(...provider.models.map((model) => new Date(model.pricing.updatedAt).getTime())) ), + changeFrequency: 'weekly', + priority: 0.7, })) const modelPages: MetadataRoute.Sitemap = ALL_CATALOG_MODELS.map((model) => ({ url: `${baseUrl}${model.href}`, lastModified: new Date(model.pricing.updatedAt), + changeFrequency: 'monthly', + priority: 0.6, })) const staticPages: MetadataRoute.Sitemap = [ { url: baseUrl, lastModified: now, + changeFrequency: 'daily', + priority: 1.0, }, { url: `${baseUrl}/blog`, @@ -52,10 +66,6 @@ export default async function sitemap(): Promise { url: `${baseUrl}/blog/tags`, lastModified: now, }, - // { - // url: `${baseUrl}/templates`, - // lastModified: now, - // }, { url: `${baseUrl}/changelog`, lastModified: now, diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx index ca48abef01b..2c0db1d6e1d 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx @@ -1,8 +1,9 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import type { Metadata } from 'next' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' -import { prefetchGeneralSettings, prefetchUserProfile } from './prefetch' +import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch' import { SettingsPage } from './settings' const SECTION_TITLES: Record = { @@ -11,6 +12,7 @@ const SECTION_TITLES: Record = { secrets: 'Secrets', 'template-profile': 'Template Profile', 'access-control': 'Access Control', + 'audit-logs': 'Audit Logs', apikeys: 'Sim Keys', byok: 'BYOK', subscription: 'Subscription', @@ -46,6 +48,7 @@ export default async function SettingsSectionPage({ void prefetchGeneralSettings(queryClient) void prefetchUserProfile(queryClient) + if (isBillingEnabled) void prefetchSubscriptionData(queryClient) return ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts index 7ac50861e5b..d04d9481d1a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts @@ -2,6 +2,7 @@ import type { QueryClient } from '@tanstack/react-query' import { headers } from 'next/headers' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { generalSettingsKeys, mapGeneralSettingsResponse } from '@/hooks/queries/general-settings' +import { subscriptionKeys } from '@/hooks/queries/subscription' import { mapUserProfileResponse, userProfileKeys } from '@/hooks/queries/user-profile' /** @@ -35,6 +36,28 @@ export function prefetchGeneralSettings(queryClient: QueryClient) { }) } +/** + * Prefetch subscription data server-side via internal API fetch. + * Uses the same query key as the client `useSubscriptionData` hook (with includeOrg=false) + * so data is shared via HydrationBoundary — ensuring the settings sidebar renders + * with the correct Team/Enterprise tabs on the first paint, with no flash. + */ +export function prefetchSubscriptionData(queryClient: QueryClient) { + return queryClient.prefetchQuery({ + queryKey: subscriptionKeys.user(false), + queryFn: async () => { + const fwdHeaders = await getForwardedHeaders() + const baseUrl = getInternalApiBaseUrl() + const response = await fetch(`${baseUrl}/api/billing?context=user`, { + headers: fwdHeaders, + }) + if (!response.ok) throw new Error(`Subscription prefetch failed: ${response.status}`) + return response.json() + }, + staleTime: 5 * 60 * 1000, + }) +} + /** * Prefetch user profile server-side via internal API fetch. * Uses the same query keys as the client `useUserProfile` hook diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index 4642fc9e843..9610babe879 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { Skeleton } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' +import { cn } from '@/lib/core/utils/cn' import { captureEvent } from '@/lib/posthog/client' import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton' import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton' @@ -27,6 +28,7 @@ import { isBillingEnabled, isCredentialSetsEnabled, } from '@/app/workspace/[workspaceId]/settings/navigation' +import { AuditLogsSkeleton } from '@/ee/audit-logs/components/audit-logs-skeleton' /** * Generic skeleton fallback for sections without a dedicated skeleton. @@ -153,6 +155,10 @@ const AccessControl = dynamic( () => import('@/ee/access-control/components/access-control').then((m) => m.AccessControl), { loading: () => } ) +const AuditLogs = dynamic( + () => import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs), + { loading: () => } +) const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO), { loading: () => , }) @@ -193,7 +199,7 @@ export function SettingsPage({ section }: SettingsPageProps) { }, [effectiveSection, sessionLoading, posthog]) return ( -
+

{label}

{effectiveSection === 'general' && } {effectiveSection === 'integrations' && } @@ -201,6 +207,7 @@ export function SettingsPage({ section }: SettingsPageProps) { {/* {effectiveSection === 'template-profile' && } */} {effectiveSection === 'credential-sets' && } {effectiveSection === 'access-control' && } + {effectiveSection === 'audit-logs' && } {effectiveSection === 'apikeys' && } {isBillingEnabled && effectiveSection === 'subscription' && } {isBillingEnabled && effectiveSection === 'team' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx b/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx index 0fab587de01..06cebe3773b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx @@ -1,7 +1,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode }) { return (
-
+
{children}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index eb6941cb10b..222bb1962aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -1,5 +1,6 @@ import { Card, + ClipboardList, Connections, HexSimple, Key, @@ -27,6 +28,7 @@ export type SettingsSection = | 'template-profile' | 'credential-sets' | 'access-control' + | 'audit-logs' | 'apikeys' | 'byok' | 'subscription' @@ -72,6 +74,8 @@ const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) const isCredentialSetsEnabled = isTruthy(getEnv('NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED')) const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED')) const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED')) +const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED')) +const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED')) export const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) export { isCredentialSetsEnabled } @@ -97,6 +101,15 @@ export const allNavigationItems: NavigationItem[] = [ requiresEnterprise: true, selfHostedOverride: isAccessControlEnabled, }, + { + id: 'audit-logs', + label: 'Audit Logs', + icon: ClipboardList, + section: 'enterprise', + requiresHosted: true, + requiresEnterprise: true, + selfHostedOverride: isAuditLogsEnabled, + }, { id: 'subscription', label: 'Subscription', @@ -171,7 +184,7 @@ export const allNavigationItems: NavigationItem[] = [ section: 'enterprise', requiresHosted: true, requiresEnterprise: true, - selfHostedOverride: isBillingEnabled, + selfHostedOverride: isWhitelabelingEnabled, }, { id: 'admin', diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index ccb7cba760b..9c1bc3fafcb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -34,7 +34,23 @@ import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' -const SKELETON_SECTIONS = [3, 2, 2] as const +const SKELETON_SECTIONS = sectionConfig + .map(({ key }) => + Math.min( + allNavigationItems.filter( + (item) => + item.section === key && + !(item.hideWhenBillingDisabled && !isBillingEnabled) && + !item.requiresTeam && + !item.requiresEnterprise && + !item.requiresSuperUser && + !item.requiresAdminRole && + item.id !== 'template-profile' + ).length, + 3 + ) + ) + .filter((count) => count > 0) interface SettingsSidebarProps { isCollapsed?: boolean @@ -61,14 +77,16 @@ export function SettingsSidebar({ const { data: session, isPending: sessionLoading } = useSession() const { data: organizationsData, isLoading: orgsLoading } = useOrganizations() const { data: generalSettings } = useGeneralSettings() - const { data: subscriptionData } = useSubscriptionData({ + const { data: subscriptionData, isLoading: subscriptionLoading } = useSubscriptionData({ enabled: isBillingEnabled, staleTime: 5 * 60 * 1000, }) - const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders() + const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders({ + enabled: !isHosted, + }) const activeOrganization = organizationsData?.activeOrganization - const { config: permissionConfig } = usePermissionConfig() + const { config: permissionConfig, isLoading: permissionLoading } = usePermissionConfig() const userEmail = session?.user?.email const userId = session?.user?.id @@ -100,9 +118,18 @@ export function SettingsSidebar({ if (item.id === 'template-profile') { return false } + if (item.id === 'integrations' && permissionConfig.hideIntegrationsTab) { + return false + } + if (item.id === 'secrets' && permissionConfig.hideSecretsTab) { + return false + } if (item.id === 'apikeys' && permissionConfig.hideApiKeysTab) { return false } + if (item.id === 'inbox' && permissionConfig.hideInboxTab) { + return false + } if (item.id === 'mcp' && permissionConfig.disableMcpTools) { return false } @@ -244,113 +271,102 @@ export function SettingsSidebar({ !isCollapsed && 'overflow-y-auto overflow-x-hidden' )} > - {sessionLoading || orgsLoading ? ( - isCollapsed ? ( - <> - {SKELETON_SECTIONS.map((count, sectionIdx) => ( -
- {Array.from({ length: count }, (_, i) => ( -
- -
- ))} -
- ))} - - ) : ( - Array.from({ length: 3 }, (_, i) => ( -
-
+ {sessionLoading || + orgsLoading || + (isBillingEnabled && subscriptionLoading) || + permissionLoading || + (!isHosted && isLoadingSSO) + ? SKELETON_SECTIONS.map((count, i) => ( +
+
- {Array.from({ length: i === 0 ? 3 : 2 }, (_, j) => ( -
- + {Array.from({ length: count }, (_, j) => ( +
+ +
))}
)) - ) - ) : ( - sectionConfig.map(({ key, title }) => { - const sectionItems = navigationItems.filter((item) => item.section === key) - if (sectionItems.length === 0) return null - - return ( -
-
-
{title}
-
-
- {sectionItems.map((item) => { - const Icon = item.icon - const active = activeSection === item.id - const isLocked = item.requiresMax && !subscriptionAccess.hasUsableMaxAccess - const itemClassName = cn( - 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-[8px] px-2 text-[14px]', - !active && 'hover-hover:bg-[var(--surface-hover)]', - active && 'bg-[var(--surface-active)]' - ) - const content = ( - <> - - - {item.label} - - {isLocked && ( - - Max + : sectionConfig.map(({ key, title }) => { + const sectionItems = navigationItems.filter((item) => item.section === key) + if (sectionItems.length === 0) return null + + return ( +
+
+
{title}
+
+
+ {sectionItems.map((item) => { + const Icon = item.icon + const active = activeSection === item.id + const isLocked = item.requiresMax && !subscriptionAccess.hasUsableMaxAccess + const itemClassName = cn( + 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-[8px] px-2 text-[14px]', + !active && 'hover-hover:bg-[var(--surface-hover)]', + active && 'bg-[var(--surface-active)]' + ) + const content = ( + <> + + + {item.label} - )} - - ) - - const element = item.externalUrl ? ( - - {content} - - ) : ( - - ) - - return ( - - {element} - - ) - })} + {isLocked && ( + + Max + + )} + + ) + + const element = item.externalUrl ? ( + + {content} + + ) : ( + + ) + + return ( + + {element} + + ) + })} +
-
- ) - }) - )} + ) + })}
!open && handleCancelDiscard()}> diff --git a/apps/sim/blocks/blocks.test.ts b/apps/sim/blocks/blocks.test.ts index 3421cb166ca..14468faaf7d 100644 --- a/apps/sim/blocks/blocks.test.ts +++ b/apps/sim/blocks/blocks.test.ts @@ -399,7 +399,6 @@ describe.concurrent('Blocks Module', () => { 'mcp-dynamic-args', 'input-format', 'response-format', - 'trigger-save', 'file-upload', 'input-mapping', 'variables-input', diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 6b808a19a4a..d8aa7bb493d 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -2561,7 +2561,6 @@ export const LinearV2Block: BlockConfig = { (sb) => !sb.id?.startsWith('webhookUrlDisplay') && !sb.id?.startsWith('webhookSecret') && - !sb.id?.startsWith('triggerSave') && !sb.id?.startsWith('triggerInstructions') && !sb.id?.startsWith('selectedTriggerId') ), diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 9cef227f35c..0d1b204e03f 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -153,10 +153,6 @@ export type SubBlockType = | 'response-format' // Response structure format | 'filter-builder' // Filter conditions builder | 'sort-builder' // Sort conditions builder - /** - * @deprecated Legacy trigger save subblock type. - */ - | 'trigger-save' // Trigger save button with validation | 'file-upload' // File uploader | 'input-mapping' // Map parent variables to child workflow input schema | 'variables-input' // Variable assignments for updating workflow variables @@ -414,8 +410,6 @@ export interface SubBlockConfig { dependsOn?: string[] | { all?: string[]; any?: string[] } // Copyable-text specific: Use webhook URL from webhook management hook useWebhookUrl?: boolean - // Trigger-save specific: The trigger ID for validation and saving - triggerId?: string // Dropdown/Combobox: Function to fetch options dynamically // Works with both 'dropdown' (select-only) and 'combobox' (editable with expression support) fetchOptions?: (blockId: string) => Promise> diff --git a/apps/sim/components/emcn/components/modal/modal.tsx b/apps/sim/components/emcn/components/modal/modal.tsx index 3e2341ce0cf..91fc76ebd93 100644 --- a/apps/sim/components/emcn/components/modal/modal.tsx +++ b/apps/sim/components/emcn/components/modal/modal.tsx @@ -102,7 +102,7 @@ ModalOverlay.displayName = 'ModalOverlay' * Each size uses viewport units with sensible min/max constraints. */ const MODAL_SIZES = { - sm: 'w-[90vw] max-w-[400px]', + sm: 'w-[90vw] max-w-[440px]', md: 'w-[90vw] max-w-[500px]', lg: 'w-[90vw] max-w-[600px]', xl: 'w-[90vw] max-w-[800px]', @@ -120,7 +120,7 @@ export interface ModalContentProps showClose?: boolean /** * Modal size variant with responsive viewport-based sizing. - * - sm: max 400px (dialogs, confirmations) + * - sm: max 440px (dialogs, confirmations) * - md: max 500px (default, forms) * - lg: max 600px (content-heavy modals) * - xl: max 800px (complex editors) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 2806cd4d314..0e4de0bf5e5 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3554,7 +3554,7 @@ export function FireworksIcon(props: SVGProps) { > ) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index cc636fb9df8..65cc642e13d 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -41,6 +41,7 @@ import { useRemovePermissionGroupMember, useUpdatePermissionGroup, } from '@/ee/access-control/hooks/permission-groups' +import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers' import { useOrganization, useOrganizations } from '@/hooks/queries/organization' import { useSubscriptionData } from '@/hooks/queries/subscription' import { PROVIDER_DEFINITIONS } from '@/providers/models' @@ -48,10 +49,19 @@ import { getAllProviderIds } from '@/providers/utils' const logger = createLogger('AccessControl') +interface OrgMember { + userId: string + user: { + name: string | null + email: string + image?: string | null + } +} + interface AddMembersModalProps { open: boolean onOpenChange: (open: boolean) => void - availableMembers: any[] + availableMembers: OrgMember[] selectedMemberIds: Set setSelectedMemberIds: React.Dispatch>> onAddMembers: () => void @@ -72,7 +82,7 @@ function AddMembersModal({ const filteredMembers = useMemo(() => { if (!searchTerm.trim()) return availableMembers const query = searchTerm.toLowerCase() - return availableMembers.filter((m: any) => { + return availableMembers.filter((m) => { const name = m.user?.name || '' const email = m.user?.email || '' return name.toLowerCase().includes(query) || email.toLowerCase().includes(query) @@ -81,12 +91,12 @@ function AddMembersModal({ const allFilteredSelected = useMemo(() => { if (filteredMembers.length === 0) return false - return filteredMembers.every((m: any) => selectedMemberIds.has(m.userId)) + return filteredMembers.every((m) => selectedMemberIds.has(m.userId)) }, [filteredMembers, selectedMemberIds]) const handleToggleAll = () => { if (allFilteredSelected) { - const filteredIds = new Set(filteredMembers.map((m: any) => m.userId)) + const filteredIds = new Set(filteredMembers.map((m) => m.userId)) setSelectedMemberIds((prev) => { const next = new Set(prev) filteredIds.forEach((id) => next.delete(id)) @@ -95,7 +105,7 @@ function AddMembersModal({ } else { setSelectedMemberIds((prev) => { const next = new Set(prev) - filteredMembers.forEach((m: any) => next.add(m.userId)) + filteredMembers.forEach((m) => next.add(m.userId)) return next }) } @@ -140,7 +150,7 @@ function AddMembersModal({ className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' />
-
@@ -152,7 +162,7 @@ function AddMembersModal({

) : (
- {filteredMembers.map((member: any) => { + {filteredMembers.map((member) => { const name = member.user?.name || 'Unknown' const email = member.user?.email || '' const avatarInitial = name.charAt(0).toUpperCase() @@ -314,16 +324,22 @@ export function AccessControl() { configKey: 'hideCopilot' as const, }, { - id: 'hide-api-keys', - label: 'API Keys', + id: 'hide-integrations', + label: 'Integrations', category: 'Settings Tabs', - configKey: 'hideApiKeysTab' as const, + configKey: 'hideIntegrationsTab' as const, }, { - id: 'hide-environment', - label: 'Environment', + id: 'hide-secrets', + label: 'Secrets', category: 'Settings Tabs', - configKey: 'hideEnvironmentTab' as const, + configKey: 'hideSecretsTab' as const, + }, + { + id: 'hide-api-keys', + label: 'API Keys', + category: 'Settings Tabs', + configKey: 'hideApiKeysTab' as const, }, { id: 'hide-files', @@ -391,6 +407,12 @@ export function AccessControl() { category: 'Collaboration', configKey: 'disableInvitations' as const, }, + { + id: 'hide-inbox', + label: 'Sim Mailer', + category: 'Features', + configKey: 'hideInboxTab' as const, + }, { id: 'disable-public-api', label: 'Public API', @@ -420,6 +442,29 @@ export function AccessControl() { return categories }, [filteredPlatformFeatures]) + const platformCategoryColumns = useMemo(() => { + const categoryGroups = [ + ['Sidebar', 'Deploy Tabs', 'Collaboration'], + ['Workflow Panel', 'Tools', 'Features'], + ['Settings Tabs', 'Logs'], + ] + + const assignedCategories = new Set(categoryGroups.flat()) + const unassigned = Object.keys(platformCategories).filter((c) => !assignedCategories.has(c)) + const groups = unassigned.length > 0 ? [...categoryGroups, unassigned] : categoryGroups + + return groups + .map((column) => + column + .map((category) => ({ + category, + features: platformCategories[category] ?? [], + })) + .filter((section) => section.features.length > 0) + ) + .filter((column) => column.length > 0) + }, [platformCategories]) + const hasConfigChanges = useMemo(() => { if (!viewingGroup || !editingConfig) return false const original = viewingGroup.config @@ -436,7 +481,14 @@ export function AccessControl() { return a.name.localeCompare(b.name) }) }, []) - const allProviderIds = useMemo(() => getAllProviderIds(), []) + const { data: blacklistedProvidersData } = useBlacklistedProviders({ enabled: showConfigModal }) + + const allProviderIds = useMemo(() => { + const allIds = getAllProviderIds() + const blacklist = blacklistedProvidersData?.blacklistedProviders ?? [] + if (blacklist.length === 0) return allIds + return allIds.filter((id) => !blacklist.includes(id.toLowerCase())) + }, [blacklistedProvidersData]) const filteredProviders = useMemo(() => { if (!providerSearchTerm.trim()) return allProviderIds @@ -450,6 +502,16 @@ export function AccessControl() { return allBlocks.filter((b) => b.name.toLowerCase().includes(query)) }, [allBlocks, integrationSearchTerm]) + const filteredCoreBlocks = useMemo(() => { + return filteredBlocks.filter((block) => block.category === 'blocks') + }, [filteredBlocks]) + + const filteredToolBlocks = useMemo(() => { + return filteredBlocks + .filter((block) => block.category === 'tools' || block.category === 'triggers') + .sort((a, b) => a.name.localeCompare(b.name)) + }, [filteredBlocks]) + const orgMembers = useMemo(() => { return organization?.members || [] }, [organization]) @@ -677,7 +739,7 @@ export function AccessControl() { const availableMembersToAdd = useMemo(() => { const existingMemberUserIds = new Set(members.map((m) => m.userId)) - return orgMembers.filter((m: any) => !existingMemberUserIds.has(m.userId)) + return orgMembers.filter((m) => !existingMemberUserIds.has(m.userId)) }, [orgMembers, members]) if (isLoading) { @@ -841,249 +903,259 @@ export function AccessControl() { } }} > - + Configure Permissions - + Model Providers Blocks Platform - - -
-
-
- - setProviderSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
- + + +
+
+ + setProviderSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
-
- {filteredProviders.map((providerId) => { - const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon - const providerName = - PROVIDER_DEFINITIONS[providerId]?.name || - providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - return ( -
- toggleProvider(providerId)} - /> -
- {ProviderIcon && } -
- {providerName} -
+
+ }} + > + {editingConfig?.allowedModelProviders === null || + allProviderIds.every((id) => + editingConfig?.allowedModelProviders?.includes(id) + ) + ? 'Deselect All' + : 'Select All'} +
-
- - - - -
-
-
- - setIntegrationSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
- -
-
- {filteredBlocks.map((block) => { - const BlockIcon = block.icon - return ( -
- toggleIntegration(block.type)} - /> -
- {BlockIcon && ( - - )} -
- {block.name} +
+ {filteredProviders.map((providerId) => { + const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon + const providerName = + PROVIDER_DEFINITIONS[providerId]?.name || + providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + const checkboxId = `provider-${providerId}` + return ( + + ) + })} +
+ + + +
+
+ + setIntegrationSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
+
- -
- - - -
-
-
- - setPlatformSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> +
+ {filteredCoreBlocks.length > 0 && ( +
+ + Core Blocks + +
+ {filteredCoreBlocks.map((block) => { + const BlockIcon = block.icon + const checkboxId = `block-${block.type}` + return ( + + ) + })} +
- -
-
- {Object.entries(platformCategories).map(([category, features]) => ( -
- - {category} - -
- {features.map((feature) => ( -
+ )} + {filteredToolBlocks.length > 0 && ( +
+ + Tools + +
+ {filteredToolBlocks.map((block) => { + const BlockIcon = block.icon + const checkboxId = `block-${block.type}` + return ( +
- ))} -
+ {BlockIcon && ( + + )} +
+ {block.name} + + ) + })}
- ))} +
+ )} +
+ + + +
+
+ + setPlatformSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
+ +
+
+ {platformCategoryColumns.map((column, columnIndex) => ( +
+ {column.map(({ category, features }) => ( +
+ + {category} + +
+ {features.map((feature) => ( + + ))} +
+
+ ))} +
+ ))}
- -
+ +
+ ) +} diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx new file mode 100644 index 00000000000..370d81e2289 --- /dev/null +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -0,0 +1,375 @@ +'use client' + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' +import { ChevronDown, RefreshCw, Search } from 'lucide-react' +import { Badge, Button, Combobox, type ComboboxOption, Skeleton } from '@/components/emcn' +import { Input } from '@/components/ui' +import { cn } from '@/lib/core/utils/cn' +import { formatDateTime } from '@/lib/core/utils/formatting' +import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format' +import { RESOURCE_TYPE_OPTIONS } from '@/ee/audit-logs/constants' +import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs' + +const logger = createLogger('AuditLogs') + +const DATE_RANGE_OPTIONS: ComboboxOption[] = [ + { label: 'Last 7 days', value: '7' }, + { label: 'Last 30 days', value: '30' }, + { label: 'Last 90 days', value: '90' }, + { label: 'All time', value: '' }, +] + +function formatResourceType(type: string): string { + return type + .split('_') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' ') +} + +function getStartOfDay(daysAgo: number): string { + const start = new Date() + start.setDate(start.getDate() - daysAgo) + start.setHours(0, 0, 0, 0) + return start.toISOString() +} + +function formatAction(action: string): string { + return action.replace(/[._]/g, ' ') +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function formatMetadataLabel(key: string): string { + return key + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/[_-]+/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase()) +} + +function formatPrimitiveValue(value: string | number | boolean | null): string { + if (value === null) return '-' + if (typeof value === 'boolean') return value ? 'Yes' : 'No' + if (typeof value === 'number') return value.toLocaleString() + return value +} + +function renderMetadataValue(value: unknown) { + if (value == null) return - + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return {formatPrimitiveValue(value)} + } + + if (Array.isArray(value)) { + if (value.length === 0) { + return None + } + + const hasComplexValues = value.some((item) => typeof item === 'object' && item !== null) + if (!hasComplexValues) { + return ( + + {value + .map((item) => formatPrimitiveValue((item as string | number | boolean | null) ?? null)) + .join(', ')} + + ) + } + + return ( +
+        {JSON.stringify(value, null, 2)}
+      
+ ) + } + + if (isRecord(value)) { + const entries = Object.entries(value).filter(([, nestedValue]) => nestedValue !== undefined) + if (entries.length === 0) { + return None + } + + const hasComplexValues = entries.some(([, nestedValue]) => { + return Array.isArray(nestedValue) || isRecord(nestedValue) + }) + + if (!hasComplexValues) { + return ( + + {entries + .map(([nestedKey, nestedValue]) => { + return `${formatMetadataLabel(nestedKey)}: ${formatPrimitiveValue((nestedValue as string | number | boolean | null) ?? null)}` + }) + .join(' · ')} + + ) + } + + return ( +
+        {JSON.stringify(value, null, 2)}
+      
+ ) + } + + return ( +
+      {JSON.stringify(value, null, 2)}
+    
+ ) +} + +function getMetadataEntries(metadata: unknown) { + if (!isRecord(metadata)) return [] + + return Object.entries(metadata).filter(([key, value]) => { + if (value === undefined) return false + return !['name', 'description'].includes(key) + }) +} + +interface ActionBadgeProps { + action: string +} + +function ActionBadge({ action }: ActionBadgeProps) { + const [, verb] = action.split('.') + const variant = + verb === 'deleted' || verb === 'removed' || verb === 'revoked' ? 'red' : 'gray-secondary' + return ( + + {formatAction(action)} + + ) +} + +interface AuditLogRowProps { + entry: EnterpriseAuditLogEntry +} + +function AuditLogRow({ entry }: AuditLogRowProps) { + const [expanded, setExpanded] = useState(false) + const timestamp = formatDateTime(new Date(entry.createdAt)) + const metadataEntries = getMetadataEntries(entry.metadata) + + return ( +
+ + {expanded && ( +
+
+
+ Resource + + {formatResourceType(entry.resourceType)} + {entry.resourceId && ( + ({entry.resourceId}) + )} + +
+ {entry.resourceName && ( +
+ Name + {entry.resourceName} +
+ )} +
+ Actor + + {entry.actorName || 'Unknown'} + {entry.actorEmail && ( + ({entry.actorEmail}) + )} + +
+ {entry.description && ( +
+ + Description + + {entry.description} +
+ )} + {metadataEntries.map(([key, value]) => ( +
+ + {formatMetadataLabel(key)} + +
{renderMetadataValue(value)}
+
+ ))} +
+
+ )} +
+ ) +} + +export function AuditLogs() { + const [resourceType, setResourceType] = useState('') + const [dateRange, setDateRange] = useState('30') + const [searchTerm, setSearchTerm] = useState('') + const [debouncedSearch, setDebouncedSearch] = useState('') + const debounceRef = useRef | null>(null) + + useEffect(() => { + const trimmed = searchTerm.trim() + if (trimmed === debouncedSearch) return + debounceRef.current = setTimeout(() => { + setDebouncedSearch(trimmed) + }, 300) + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current) + } + }, [searchTerm, debouncedSearch]) + + const filters = useMemo(() => { + return { + search: debouncedSearch || undefined, + resourceType: resourceType || undefined, + startDate: dateRange ? getStartOfDay(Number(dateRange)) : undefined, + } + }, [debouncedSearch, resourceType, dateRange]) + + const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage, refetch, isRefetching } = + useAuditLogs(filters) + + const allEntries = useMemo(() => { + if (!data?.pages) return [] + return data.pages.flatMap((page) => page.data) + }, [data]) + + const handleRefresh = useCallback(() => { + refetch().catch((error: unknown) => { + logger.error('Failed to refresh audit logs', { error }) + }) + }, [refetch]) + + const handleLoadMore = useCallback(() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage().catch((error: unknown) => { + logger.error('Failed to load more audit logs', { error }) + }) + } + }, [hasNextPage, isFetchingNextPage, fetchNextPage]) + + return ( +
+
+
+ + setSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
+
+ +
+
+ +
+ +
+ +
+
+ Timestamp + Event + Description + Actor +
+ +
+ {isLoading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+
+ + + + +
+
+ ))} +
+ ) : allEntries.length === 0 ? ( +
+ {debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'} +
+ ) : ( +
+ {allEntries.map((entry) => ( + + ))} + {hasNextPage && ( +
+ +
+ )} +
+ )} +
+
+
+ ) +} diff --git a/apps/sim/ee/audit-logs/constants.ts b/apps/sim/ee/audit-logs/constants.ts new file mode 100644 index 00000000000..445265f4f81 --- /dev/null +++ b/apps/sim/ee/audit-logs/constants.ts @@ -0,0 +1,24 @@ +import type { ComboboxOption } from '@/components/emcn' +import { AuditResourceType } from '@/lib/audit/types' + +const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAUTH']) + +const DISPLAY_OVERRIDES: Record = { OAUTH: 'OAuth' } + +function formatResourceLabel(key: string): string { + return key + .split('_') + .map((w) => { + const upper = w.toUpperCase() + if (ACRONYMS.has(upper)) return DISPLAY_OVERRIDES[upper] ?? upper + return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() + }) + .join(' ') +} + +export const RESOURCE_TYPE_OPTIONS: ComboboxOption[] = [ + { label: 'All Types', value: '' }, + ...(Object.entries(AuditResourceType) as [string, string][]) + .map(([key, value]) => ({ label: formatResourceLabel(key), value })) + .sort((a, b) => a.label.localeCompare(b.label)), +] diff --git a/apps/sim/ee/audit-logs/hooks/audit-logs.ts b/apps/sim/ee/audit-logs/hooks/audit-logs.ts new file mode 100644 index 00000000000..259d6094c0d --- /dev/null +++ b/apps/sim/ee/audit-logs/hooks/audit-logs.ts @@ -0,0 +1,58 @@ +import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' +import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format' + +export const auditLogKeys = { + all: ['audit-logs'] as const, + lists: () => [...auditLogKeys.all, 'list'] as const, + list: (filters: AuditLogFilters) => [...auditLogKeys.lists(), filters] as const, +} + +export interface AuditLogFilters { + search?: string + action?: string + resourceType?: string + actorId?: string + startDate?: string + endDate?: string +} + +interface AuditLogPage { + success: boolean + data: EnterpriseAuditLogEntry[] + nextCursor?: string +} + +async function fetchAuditLogs( + filters: AuditLogFilters, + cursor?: string, + signal?: AbortSignal +): Promise { + const params = new URLSearchParams() + params.set('limit', '50') + if (filters.search) params.set('search', filters.search) + if (filters.action) params.set('action', filters.action) + if (filters.resourceType) params.set('resourceType', filters.resourceType) + if (filters.actorId) params.set('actorId', filters.actorId) + if (filters.startDate) params.set('startDate', filters.startDate) + if (filters.endDate) params.set('endDate', filters.endDate) + if (cursor) params.set('cursor', cursor) + + const response = await fetch(`/api/audit-logs?${params.toString()}`, { signal }) + if (!response.ok) { + const body = await response.json().catch(() => ({})) + throw new Error(body.error || `Failed to fetch audit logs: ${response.status}`) + } + return response.json() +} + +export function useAuditLogs(filters: AuditLogFilters, enabled = true) { + return useInfiniteQuery({ + queryKey: auditLogKeys.list(filters), + queryFn: ({ pageParam, signal }) => fetchAuditLogs(filters, pageParam, signal), + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage) => lastPage.nextCursor, + enabled, + staleTime: 30 * 1000, + placeholderData: keepPreviousData, + }) +} diff --git a/apps/sim/ee/sso/hooks/sso.ts b/apps/sim/ee/sso/hooks/sso.ts index 2dfa1592ea4..37c20ffb7fb 100644 --- a/apps/sim/ee/sso/hooks/sso.ts +++ b/apps/sim/ee/sso/hooks/sso.ts @@ -14,8 +14,8 @@ export const ssoKeys = { /** * Fetch SSO providers */ -async function fetchSSOProviders() { - const response = await fetch('/api/auth/sso/providers') +async function fetchSSOProviders(signal: AbortSignal) { + const response = await fetch('/api/auth/sso/providers', { signal }) if (!response.ok) { throw new Error('Failed to fetch SSO providers') } @@ -25,12 +25,17 @@ async function fetchSSOProviders() { /** * Hook to fetch SSO providers */ -export function useSSOProviders() { +interface UseSSOProvidersOptions { + enabled?: boolean +} + +export function useSSOProviders({ enabled = true }: UseSSOProvidersOptions = {}) { return useQuery({ queryKey: ssoKeys.providers(), - queryFn: fetchSSOProviders, - staleTime: 5 * 60 * 1000, // 5 minutes + queryFn: ({ signal }) => fetchSSOProviders(signal), + staleTime: 5 * 60 * 1000, placeholderData: keepPreviousData, + enabled, }) } diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index a6946772c9a..fa28f3d85b8 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' import { Loader2, X } from 'lucide-react' import Image from 'next/image' -import { Button, Input, Label, Switch } from '@/components/emcn' +import { Button, Input, Label } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client/utils' import { HEX_COLOR_REGEX } from '@/lib/branding' @@ -79,6 +79,22 @@ interface ColorInputProps { function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorInputProps) { const isValidHex = !value || HEX_COLOR_REGEX.test(value) + const handleChange = useCallback( + (e: React.ChangeEvent) => { + let v = e.target.value.trim() + if (v && !v.startsWith('#')) { + v = `#${v}` + } + v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '') + onChange(v.slice(0, 7)) + }, + [onChange] + ) + + const handleFocus = useCallback((e: React.FocusEvent) => { + e.target.select() + }, []) + return (
@@ -92,7 +108,8 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn
onChange(e.target.value)} + onChange={handleChange} + onFocus={handleFocus} placeholder={placeholder} className={cn( 'h-[36px] font-mono text-[13px]', @@ -154,7 +171,6 @@ export function WhitelabelingSettings() { const [documentationUrl, setDocumentationUrl] = useState('') const [termsUrl, setTermsUrl] = useState('') const [privacyUrl, setPrivacyUrl] = useState('') - const [hidePoweredBySim, setHidePoweredBySim] = useState(false) const [logoUrl, setLogoUrl] = useState(null) const [wordmarkUrl, setWordmarkUrl] = useState(null) const [formInitialized, setFormInitialized] = useState(false) @@ -172,7 +188,6 @@ export function WhitelabelingSettings() { setDocumentationUrl(savedSettings.documentationUrl ?? '') setTermsUrl(savedSettings.termsUrl ?? '') setPrivacyUrl(savedSettings.privacyUrl ?? '') - setHidePoweredBySim(savedSettings.hidePoweredBySim ?? false) setLogoUrl(savedSettings.logoUrl ?? null) setWordmarkUrl(savedSettings.wordmarkUrl ?? null) setFormInitialized(true) @@ -222,7 +237,6 @@ export function WhitelabelingSettings() { documentationUrl: documentationUrl || null, termsUrl: termsUrl || null, privacyUrl: privacyUrl || null, - hidePoweredBySim, } try { @@ -246,7 +260,6 @@ export function WhitelabelingSettings() { documentationUrl, termsUrl, privacyUrl, - hidePoweredBySim, ]) if (isBillingEnabled) { @@ -496,21 +509,6 @@ export function WhitelabelingSettings() {
-
- Advanced -
-
- - Hide "Powered by Sim" branding - - - Removes the Sim logo from deployed chats and forms. - -
- -
-
-