fix(landing): update broken links, change colors (#3687)

* fix(landing): update broken links, change colors

* update integration pages

* update icons

* link to tag

* fix(landing): resolve build errors and address PR review comments

- Extract useEffect redirect into ExternalRedirect client component to fix
  fs/promises bundling error in privacy/terms server pages
- Fix InfisicalIcon fill='black' → fill='currentColor' for theme compatibility
- Add target="_blank" + rel="noopener noreferrer" to enterprise Typeform link
- Install @types/micromatch to fix missing type declarations build error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(icons): fix InfisicalIcon fill='black' → fill='currentColor' in docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* remove hardcoded ff

* fix(generate-docs): fix tool description extraction for two-step and name-mismatch patterns

Replace the fragile first-id/first-description heuristic with a per-id
window search: for each id: 'tool_id' match, scan the next 600 chars
(stopping before any params: block) for description: and name: fields.
This correctly handles the two-step pattern used by Intercom and others
where the ToolConfig export comes after a separate base object whose
params: would have cut off the old approach.

Add an exact-name fallback that checks tools.access for a tool whose
name matches the operation label — handles cases where block op IDs are
short aliases (e.g. Slack 'send') while the tool ID is more descriptive
('slack_message') but the tool name 'Slack Message' still differs.

Remove the word-overlap scoring fallback which was producing incorrect
descriptions (Intercom all saying 'Intercom API access token', Reddit
Save/Unsave inverted, etc.).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-03-19 20:22:55 -07:00
committed by GitHub
parent 6326353f5c
commit fa181f0155
30 changed files with 737 additions and 292 deletions

View File

@@ -4140,7 +4140,7 @@ export function IncidentioIcon(props: SVGProps<SVGSVGElement>) {
export function InfisicalIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 273 182' xmlns='http://www.w3.org/2000/svg'>
<svg {...props} viewBox='20 25 233 132' xmlns='http://www.w3.org/2000/svg'>
<path
d='m191.6 39.4c-20.3 0-37.15 13.21-52.9 30.61-12.99-16.4-29.8-30.61-51.06-30.61-27.74 0-50.44 23.86-50.44 51.33 0 26.68 21.43 51.8 48.98 51.8 20.55 0 37.07-13.86 51.32-31.81 12.69 16.97 29.1 31.41 53.2 31.41 27.13 0 49.85-22.96 49.85-51.4 0-27.12-20.44-51.33-48.95-51.33zm-104.3 77.94c-14.56 0-25.51-12.84-25.51-26.07 0-13.7 10.95-28.29 25.51-28.29 14.93 0 25.71 11.6 37.6 27.34-11.31 15.21-22.23 27.02-37.6 27.02zm104.4 0.25c-15 0-25.28-11.13-37.97-27.37 12.69-16.4 22.01-27.24 37.59-27.24 14.97 0 24.79 13.25 24.79 27.26 0 13-10.17 27.35-24.41 27.35z'
fill='currentColor'
@@ -4253,7 +4253,7 @@ export function ZoomIcon(props: SVGProps<SVGSVGElement>) {
fill='currentColor'
width='800px'
height='800px'
viewBox='0 0 32 32'
viewBox='-1 9.5 34 13'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
>

View File

@@ -5,7 +5,7 @@ description: Manage users and groups in Azure AD (Microsoft Entra ID)
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
<BlockInfoCard
type="microsoft_ad"
color="#0078D4"
/>

View File

@@ -5,7 +5,7 @@ description: Manage users and groups in Okta
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
<BlockInfoCard
type="okta"
color="#191919"
/>
@@ -29,6 +29,7 @@ In Sim, the Okta integration enables your agents to automate identity management
If you encounter issues with the Okta integration, contact us at [help@sim.ai](mailto:help@sim.ai)
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership.

View File

@@ -612,7 +612,9 @@ export default function Enterprise() {
Ready for growth?
</p>
<Link
href='/contact'
href='https://form.typeform.com/to/jqCO12pF'
target='_blank'
rel='noopener noreferrer'
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-white bg-white px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Book a demo

View File

@@ -78,7 +78,7 @@ const PRICING_TIERS: PricingTier[] = [
'SSO & SCIM · SOC2 & HIPAA',
'Self hosting · Dedicated support',
],
cta: { label: 'Book a demo', href: '/contact' },
cta: { label: 'Book a demo', href: 'https://form.typeform.com/to/jqCO12pF' },
},
]
@@ -125,12 +125,14 @@ function PricingCard({ tier }: PricingCardProps) {
</p>
<div className='mt-4'>
{isEnterprise ? (
<Link
<a
href={tier.cta.href}
target='_blank'
rel='noopener noreferrer'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
>
{tier.cta.label}
</Link>
</a>
) : isPro ? (
<Link
href={tier.cta.href}

View File

@@ -1,4 +1,4 @@
import { getAllPostMeta } from '@/lib/blog/registry'
import { getNavBlogPosts } from '@/lib/blog/registry'
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import { season } from '@/app/_styles/fonts/season/season'
import {
@@ -33,12 +33,7 @@ import {
* enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials).
*/
export default async function Landing() {
const allPosts = await getAllPostMeta()
const featuredPost = allPosts.find((p) => p.featured) ?? allPosts[0]
const recentPosts = allPosts.filter((p) => p !== featuredPost).slice(0, 4)
const blogPosts = [featuredPost, ...recentPosts]
.filter(Boolean)
.map((p) => ({ slug: p.slug, title: p.title, ogImage: p.ogImage }))
const blogPosts = await getNavBlogPosts()
return (
<div className={`${season.variable} ${martianMono.variable} min-h-screen bg-[#1C1C1C]`}>

View File

@@ -1,7 +1,9 @@
import { getNavBlogPosts } from '@/lib/blog/registry'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
export default function StudioLayout({ children }: { children: React.ReactNode }) {
export default async function StudioLayout({ children }: { children: React.ReactNode }) {
const blogPosts = await getNavBlogPosts()
const orgJsonLd = {
'@context': 'https://schema.org',
'@type': 'Organization',
@@ -34,7 +36,7 @@ export default function StudioLayout({ children }: { children: React.ReactNode }
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
/>
<header>
<Navbar />
<Navbar blogPosts={blogPosts} />
</header>
<main className='relative flex-1'>{children}</main>
<Footer />

View File

@@ -0,0 +1,18 @@
'use client'
import { useEffect } from 'react'
interface ExternalRedirectProps {
url: string
}
/** Redirects to an external URL when it is configured via an environment variable. */
export default function ExternalRedirect({ url }: ExternalRedirectProps) {
useEffect(() => {
if (url?.startsWith('http')) {
window.location.href = url
}
}, [url])
return null
}

View File

@@ -1,4 +1,5 @@
import Background from '@/app/(landing)/components/background/background'
import ExternalRedirect from '@/app/(landing)/components/external-redirect'
import Footer from '@/app/(landing)/components/footer/footer'
import Hero from '@/app/(landing)/components/hero/hero'
import Integrations from '@/app/(landing)/components/integrations/integrations'
@@ -20,4 +21,5 @@ export {
Footer,
StructuredData,
LegalLayout,
ExternalRedirect,
}

View File

@@ -1,3 +1,4 @@
import { getNavBlogPosts } from '@/lib/blog/registry'
import { isHosted } from '@/lib/core/config/feature-flags'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
@@ -7,11 +8,13 @@ interface LegalLayoutProps {
children: React.ReactNode
}
export default function LegalLayout({ title, children }: LegalLayoutProps) {
export default async function LegalLayout({ title, children }: LegalLayoutProps) {
const blogPosts = await getNavBlogPosts()
return (
<main className='min-h-screen bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
<header>
<Navbar />
<Navbar blogPosts={blogPosts} />
</header>
<div className='mx-auto max-w-[800px] px-6 pt-[60px] pb-[80px] sm:px-12'>

View File

@@ -5,7 +5,6 @@ import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/templat
import { IntegrationIcon } from '../components/integration-icon'
import { blockTypeToIconMap } from '../data/icon-mapping'
import integrations from '../data/integrations.json'
import { POPULAR_WORKFLOWS } from '../data/popular-workflows'
import type { AuthType, FAQItem, Integration } from '../data/types'
import { IntegrationFAQ } from './components/integration-faq'
import { TemplateCardButton } from './components/template-card-button'
@@ -14,44 +13,52 @@ const allIntegrations = integrations as Integration[]
const INTEGRATION_COUNT = allIntegrations.length
/** Fast O(1) lookups — avoids repeated linear scans inside render loops. */
const byName = new Map(allIntegrations.map((i) => [i.name, i]))
const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
const byType = new Map(allIntegrations.map((i) => [i.type, i]))
/** Returns workflow pairs that feature the given integration on either side. */
function getPairsFor(name: string) {
return POPULAR_WORKFLOWS.filter((p) => p.from === name || p.to === name)
}
/**
* Returns up to `limit` related integration slugs.
*
* Scoring:
* +100 — integration appears as a workflow pair partner (explicit editorial signal)
* +N — N operation names shared with the current integration (semantic similarity)
* Scoring (additive):
* +3 per shared operation name — strongest signal (same capability)
* +2 per shared operation word — weaker signal (e.g. both have "create" ops)
* +1 same auth type — comparable setup experience
*
* This means genuine partners always rank first; operation-similar integrations
* (e.g. Slack → Teams → Discord for "Send Message") fill the rest organically.
* Every integration gets a score, so the sidebar always has suggestions.
* Ties are broken by alphabetical slug order for determinism.
*/
function getRelatedSlugs(
name: string,
slug: string,
operations: Integration['operations'],
authType: AuthType,
limit = 6
): string[] {
const partners = new Set(getPairsFor(name).map((p) => (p.from === name ? p.to : p.from)))
const currentOps = new Set(operations.map((o) => o.name.toLowerCase()))
const currentOpNames = new Set(operations.map((o) => o.name.toLowerCase()))
const currentOpWords = new Set(
operations.flatMap((o) =>
o.name
.toLowerCase()
.split(/\s+/)
.filter((w) => w.length > 3)
)
)
return allIntegrations
.filter((i) => i.slug !== slug)
.map((i) => ({
slug: i.slug,
score:
(partners.has(i.name) ? 100 : 0) +
i.operations.filter((o) => currentOps.has(o.name.toLowerCase())).length,
}))
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)
.map((i) => {
const sharedNames = i.operations.filter((o) =>
currentOpNames.has(o.name.toLowerCase())
).length
const sharedWords = i.operations.filter((o) =>
o.name
.toLowerCase()
.split(/\s+/)
.some((w) => w.length > 3 && currentOpWords.has(w))
).length
const sameAuth = i.authType === authType ? 1 : 0
return { slug: i.slug, score: sharedNames * 3 + sharedWords * 2 + sameAuth }
})
.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug))
.slice(0, limit)
.map(({ slug: s }) => s)
}
@@ -70,7 +77,6 @@ function buildFAQs(integration: Integration): FAQItem[] {
const { name, description, operations, triggers, authType } = integration
const topOps = operations.slice(0, 5)
const topOpNames = topOps.map((o) => o.name)
const pairs = getPairsFor(name)
const authStep = AUTH_STEP[authType]
const faqs: FAQItem[] = [
@@ -89,6 +95,10 @@ function buildFAQs(integration: Integration): FAQItem[] {
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.`,
},
{
question: `Can I use ${name} as a tool inside an AI agent in Sim?`,
answer: `Yes — this is one of Sim's core capabilities. Instead of hard-coding when and how ${name} is used, you give an AI agent access to ${name} tools and describe the goal in plain language. The agent decides which tools to call, in what order, and how to handle the results. This means your automation adapts to context rather than breaking when inputs change.`,
},
...(topOpNames.length >= 2
? [
{
@@ -97,19 +107,15 @@ function buildFAQs(integration: Integration): FAQItem[] {
},
]
: []),
...(pairs.length > 0
? [
{
question: `Can I connect ${name} to ${pairs[0].from === name ? pairs[0].to : pairs[0].from} with Sim?`,
answer: `Yes. ${pairs[0].description} In Sim, you set this up by adding both a ${name} block and a ${pairs[0].from === name ? pairs[0].to : pairs[0].from} block to the same workflow and connecting them through an AI agent that orchestrates the logic between them.`,
},
]
: []),
...(triggers.length > 0
? [
{
question: `Can ${name} trigger a Sim workflow automatically?`,
answer: `Yes. ${name} supports ${triggers.length} webhook trigger${triggers.length === 1 ? '' : 's'} that can instantly start a Sim workflow: ${triggers.map((t) => t.name).join(', ')}. No polling needed — the workflow fires the moment the event occurs in ${name}.`,
question: `How do I trigger a Sim workflow from ${name} automatically?`,
answer: `In your Sim workflow, switch the ${name} block to Trigger mode 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: `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.`,
},
]
: []),
@@ -190,11 +196,10 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
const IconComponent = blockTypeToIconMap[integration.type]
const faqs = buildFAQs(integration)
const relatedSlugs = getRelatedSlugs(name, slug, operations)
const relatedSlugs = getRelatedSlugs(slug, operations, authType)
const relatedIntegrations = relatedSlugs
.map((s) => bySlug.get(s))
.filter((i): i is Integration => i !== undefined)
const featuredPairs = getPairsFor(name)
const baseType = integration.type.replace(/_v\d+$/, '')
const matchingTemplates = TEMPLATES.filter(
(t) =>
@@ -420,15 +425,18 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
{triggers.length > 0 && (
<section aria-labelledby='triggers-heading'>
<h2 id='triggers-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
Triggers
Real-time triggers
</h2>
<p className='mb-6 text-[#999] text-[14px]'>
These events in {name} can automatically start a Sim workflow no polling
required.
<p className='mb-4 text-[#999] text-[14px] leading-relaxed'>
Connect a {name} webhook to Sim and your workflow fires the instant an event
happens no polling, no delay. Sim receives the full event payload and makes
every field available as a variable inside your workflow.
</p>
{/* Event cards */}
<ul
className='grid grid-cols-1 gap-3 sm:grid-cols-2'
aria-label={`${name} triggers`}
aria-label={`${name} trigger events`}
>
{triggers.map((trigger) => (
<li
@@ -447,7 +455,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
>
<polygon points='13 2 3 14 12 14 11 22 21 10 12 10 13 2' />
</svg>
Trigger
Event
</span>
</div>
<p className='font-[500] text-[#ECECEC] text-[13px]'>{trigger.name}</p>
@@ -462,73 +470,6 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
</section>
)}
{/* Popular workflows featuring this integration */}
{featuredPairs.length > 0 && (
<section aria-labelledby='workflows-heading'>
<h2 id='workflows-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
Popular workflows with {name}
</h2>
<p className='mb-6 text-[#999] text-[14px]'>
Common automation patterns teams build on Sim using {name}.
</p>
<ul
className='grid grid-cols-1 gap-4 sm:grid-cols-2'
aria-label='Popular workflow combinations'
>
{featuredPairs.map(({ from, to, headline, description: desc }) => {
const fromInt = byName.get(from)
const toInt = byName.get(to)
const FromIcon = fromInt ? blockTypeToIconMap[fromInt.type] : undefined
const ToIcon = toInt ? blockTypeToIconMap[toInt.type] : undefined
const fromBg = fromInt?.bgColor ?? '#6B7280'
const toBg = toInt?.bgColor ?? '#6B7280'
return (
<li key={`${from}-${to}`}>
<div className='h-full rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
<div className='mb-3 flex items-center gap-2 text-[12px]'>
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
{FromIcon && (
<IntegrationIcon
bgColor={fromBg}
name={from}
Icon={FromIcon}
as='span'
className='h-3.5 w-3.5 rounded-[2px]'
iconClassName='h-2.5 w-2.5'
aria-hidden='true'
/>
)}
{from}
</span>
<span className='text-[#555]' aria-hidden='true'>
</span>
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
{ToIcon && (
<IntegrationIcon
bgColor={toBg}
name={to}
Icon={ToIcon}
as='span'
className='h-3.5 w-3.5 rounded-[2px]'
iconClassName='h-2.5 w-2.5'
aria-hidden='true'
/>
)}
{to}
</span>
</div>
<p className='mb-1 font-[500] text-[#ECECEC] text-[14px]'>{headline}</p>
<p className='text-[#999] text-[13px] leading-relaxed'>{desc}</p>
</div>
</li>
)
})}
</ul>
</section>
)}
{/* Workflow templates */}
{matchingTemplates.length > 0 && (
<section aria-labelledby='templates-heading'>
@@ -539,7 +480,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
Ready-to-use workflows featuring {name}. Click any to build it instantly.
</p>
<ul
className='grid grid-cols-1 gap-3 sm:grid-cols-2'
className='grid grid-cols-1 gap-4 sm:grid-cols-2'
aria-label='Workflow templates'
>
{matchingTemplates.map((template) => {
@@ -551,34 +492,49 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
return (
<li key={template.title}>
<TemplateCardButton prompt={template.prompt}>
{/* Integration icons */}
<div className='mb-3 flex items-center gap-1.5'>
{allTypes.map((bt) => {
const int = byType.get(bt)
{/* Integration pills row */}
<div className='mb-3 flex flex-wrap items-center gap-1.5 text-[12px]'>
{allTypes.map((bt, idx) => {
// Templates may use unversioned keys (e.g. "notion") while the
// icon map has versioned keys ("notion_v2") — fall back to _v2.
const resolvedBt = byType.get(bt)
? bt
: byType.get(`${bt}_v2`)
? `${bt}_v2`
: bt
const int = byType.get(resolvedBt)
const intName = int?.name ?? bt
return (
<IntegrationIcon
key={bt}
bgColor={int?.bgColor ?? '#6B7280'}
name={intName}
Icon={blockTypeToIconMap[bt]}
className='h-6 w-6 rounded-[5px]'
iconClassName='h-3.5 w-3.5'
fallbackClassName='text-[9px]'
title={intName}
aria-label={intName}
/>
<span key={bt} className='inline-flex items-center gap-1.5'>
{idx > 0 && (
<span className='text-[#555]' aria-hidden='true'>
</span>
)}
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
<IntegrationIcon
bgColor={int?.bgColor ?? '#6B7280'}
name={intName}
Icon={blockTypeToIconMap[resolvedBt]}
as='span'
className='h-3.5 w-3.5 rounded-[2px]'
iconClassName='h-2.5 w-2.5'
aria-hidden='true'
/>
{intName}
</span>
</span>
)
})}
</div>
<p className='mb-3 font-[500] text-[#ECECEC] text-[13px] leading-snug'>
<p className='mb-1 font-[500] text-[#ECECEC] text-[14px]'>
{template.title}
</p>
<span className='text-[#555] text-[12px] transition-colors group-hover:text-[#999]'>
<p className='mt-3 text-[#555] text-[13px] transition-colors group-hover:text-[#999]'>
Try this workflow
</span>
</p>
</TemplateCardButton>
</li>
)
@@ -660,40 +616,34 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
<dd className='text-[#ECECEC]'>Free to start</dd>
</div>
</dl>
<a
href='https://sim.ai'
className='mt-5 flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] font-[430] font-season text-[#1C1C1C] text-[13px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Get started free
</a>
</div>
{/* Docs */}
<div className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
<h3 className='mb-2 font-[500] text-[#ECECEC] text-[14px]'>Documentation</h3>
<p className='mb-4 text-[#999] text-[13px] leading-relaxed'>
Full API reference, authentication setup, and usage examples for {name}.
</p>
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-1.5 text-[#999] text-[13px] transition-colors hover:text-[#ECECEC]'
>
docs.sim.ai
<svg
aria-hidden='true'
className='h-3 w-3'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
<div className='mt-5 flex flex-col gap-2'>
<a
href='https://sim.ai'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] font-[430] font-season text-[#1C1C1C] text-[13px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
<polyline points='15 3 21 3 21 9' />
<line x1='10' x2='21' y1='14' y2='3' />
</svg>
</a>
Get started free
</a>
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
className='flex h-[32px] w-full items-center justify-center gap-1.5 rounded-[5px] border border-[#3d3d3d] font-[430] font-season text-[#ECECEC] text-[13px] transition-colors hover:bg-[#2A2A2A]'
>
View docs
<svg
aria-hidden='true'
className='h-3 w-3'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
<polyline points='15 3 21 3 21 9' />
<line x1='10' x2='21' y1='14' y2='3' />
</svg>
</a>
</div>
</div>
{/* Related integrations — internal linking for SEO */}
@@ -738,6 +688,43 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
aria-labelledby='cta-heading'
className='mt-20 rounded-xl border border-[#2A2A2A] bg-[#242424] p-8 text-center sm:p-12'
>
{/* Logo pair: Sim × Integration */}
<div className='mx-auto mb-6 flex items-center justify-center gap-3'>
<img
src='/brandbook/logo/small.png'
alt='Sim'
className='h-14 w-14 shrink-0 rounded-xl'
/>
<div className='flex items-center gap-2'>
<span className='h-px w-5 bg-[#3d3d3d]' aria-hidden='true' />
<span
className='flex h-7 w-7 items-center justify-center rounded-full border border-[#3d3d3d]'
aria-hidden='true'
>
<svg
className='h-3.5 w-3.5 text-[#666]'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth={2}
strokeLinecap='round'
>
<path d='M5 12h14' />
<path d='M12 5v14' />
</svg>
</span>
<span className='h-px w-5 bg-[#3d3d3d]' aria-hidden='true' />
</div>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-14 w-14 rounded-xl'
iconClassName='h-7 w-7'
fallbackClassName='text-[22px]'
aria-hidden='true'
/>
</div>
<h2
id='cta-heading'
className='mb-3 font-[500] text-[#ECECEC] text-[28px] sm:text-[34px]'

View File

@@ -1,6 +1,5 @@
import type { ComponentType, ElementType, HTMLAttributes, SVGProps } from 'react'
import { cn } from '@/lib/core/utils/cn'
import { isLightBg } from '@/app/(landing)/integrations/data/utils'
interface IntegrationIconProps extends HTMLAttributes<HTMLElement> {
bgColor: string
@@ -33,9 +32,6 @@ export function IntegrationIcon({
as: Tag = 'div',
...rest
}: IntegrationIconProps) {
const isLight = isLightBg(bgColor)
const fgColor = isLight ? 'text-[#1C1C1C]' : 'text-white'
return (
<Tag
className={cn('flex shrink-0 items-center justify-center', className)}
@@ -43,9 +39,9 @@ export function IntegrationIcon({
{...rest}
>
{Icon ? (
<Icon className={cn(iconClassName, fgColor)} />
<Icon className={cn(iconClassName, 'text-white')} />
) : (
<span className={cn('font-[500] leading-none', fallbackClassName, fgColor)}>
<span className={cn('font-[500] text-white leading-none', fallbackClassName)}>
{name.charAt(0)}
</span>
)}

View File

@@ -0,0 +1,179 @@
'use client'
import { useCallback, useState } from 'react'
import {
Button,
Input,
Label,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Textarea,
} from '@/components/emcn'
type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error'
export function RequestIntegrationModal() {
const [open, setOpen] = useState(false)
const [status, setStatus] = useState<SubmitStatus>('idle')
const [integrationName, setIntegrationName] = useState('')
const [email, setEmail] = useState('')
const [useCase, setUseCase] = useState('')
const resetForm = useCallback(() => {
setIntegrationName('')
setEmail('')
setUseCase('')
setStatus('idle')
}, [])
const handleOpenChange = useCallback(
(nextOpen: boolean) => {
setOpen(nextOpen)
if (!nextOpen) resetForm()
},
[resetForm]
)
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault()
if (!integrationName.trim() || !email.trim()) return
setStatus('submitting')
try {
const res = await fetch('/api/help/integration-request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
integrationName: integrationName.trim(),
email: email.trim(),
useCase: useCase.trim() || undefined,
}),
})
if (!res.ok) throw new Error('Request failed')
setStatus('success')
setTimeout(() => setOpen(false), 1500)
} catch {
setStatus('error')
}
},
[integrationName, email, useCase]
)
const canSubmit = integrationName.trim() && email.trim() && status === 'idle'
return (
<>
<button
type='button'
onClick={() => setOpen(true)}
className='inline-flex h-[32px] shrink-0 items-center gap-[6px] rounded-[5px] border border-[#3d3d3d] px-[10px] font-[430] font-season text-[#ECECEC] text-[14px] transition-colors hover:bg-[#2A2A2A]'
>
Request an integration
</button>
<Modal open={open} onOpenChange={handleOpenChange}>
<ModalContent size='sm'>
<ModalHeader>Request an Integration</ModalHeader>
{status === 'success' ? (
<ModalBody>
<div className='flex flex-col items-center gap-3 py-6 text-center'>
<div className='flex h-10 w-10 items-center justify-center rounded-full bg-[#33C482]/10'>
<svg
className='h-5 w-5 text-[#33C482]'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth={2}
strokeLinecap='round'
strokeLinejoin='round'
>
<polyline points='20 6 9 17 4 12' />
</svg>
</div>
<p className='text-[14px] text-[var(--text-primary)]'>
Request submitted we&apos;ll follow up at{' '}
<span className='font-medium'>{email}</span>.
</p>
</div>
</ModalBody>
) : (
<form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
<ModalBody>
<div className='space-y-[12px]'>
<div className='flex flex-col gap-[4px]'>
<Label htmlFor='integration-name'>Integration name</Label>
<Input
id='integration-name'
placeholder='e.g. Stripe, HubSpot, Snowflake'
value={integrationName}
onChange={(e) => setIntegrationName(e.target.value)}
maxLength={200}
autoComplete='off'
required
/>
</div>
<div className='flex flex-col gap-[4px]'>
<Label htmlFor='requester-email'>Your email</Label>
<Input
id='requester-email'
type='email'
placeholder='you@company.com'
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete='email'
required
/>
</div>
<div className='flex flex-col gap-[4px]'>
<Label htmlFor='use-case'>
Use case <span className='text-[var(--text-tertiary)]'>(optional)</span>
</Label>
<Textarea
id='use-case'
placeholder='What would you automate with this integration?'
value={useCase}
onChange={(e) => setUseCase(e.target.value)}
rows={3}
maxLength={2000}
/>
</div>
{status === 'error' && (
<p className='text-[13px] text-[var(--text-error)]'>
Something went wrong. Please try again.
</p>
)}
</div>
</ModalBody>
<ModalFooter>
<Button
type='button'
variant='default'
onClick={() => setOpen(false)}
disabled={status === 'submitting'}
>
Cancel
</Button>
<Button type='submit' variant='primary' disabled={!canSubmit && status !== 'error'}>
{status === 'submitting' ? 'Submitting...' : 'Submit request'}
</Button>
</ModalFooter>
</form>
)}
</ModalContent>
</Modal>
</>
)
}

View File

@@ -16,6 +16,7 @@ import {
AsanaIcon,
AshbyIcon,
AttioIcon,
AzureIcon,
BoxCompanyIcon,
BrainIcon,
BrandfetchIcon,
@@ -81,6 +82,7 @@ import {
HunterIOIcon,
ImageIcon,
IncidentioIcon,
InfisicalIcon,
IntercomIcon,
JinaAIIcon,
JiraIcon,
@@ -252,6 +254,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
image_generator: ImageIcon,
imap: MailServerIcon,
incidentio: IncidentioIcon,
infisical: InfisicalIcon,
intercom_v2: IntercomIcon,
jina: JinaAIIcon,
jira: JiraIcon,
@@ -269,6 +272,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
mailgun: MailgunIcon,
mem0: Mem0Icon,
memory: BrainIcon,
microsoft_ad: AzureIcon,
microsoft_dataverse: MicrosoftDataverseIcon,
microsoft_excel_v2: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,

View File

@@ -1122,6 +1122,75 @@
"authType": "none",
"category": "tools"
},
{
"type": "microsoft_ad",
"slug": "azure-ad",
"name": "Azure AD",
"description": "Manage users and groups in Azure AD (Microsoft Entra ID)",
"longDescription": "Integrate Azure Active Directory into your workflows. List, create, update, and delete users and groups. Manage group memberships programmatically.",
"bgColor": "#0078D4",
"iconName": "AzureIcon",
"docsUrl": "https://docs.sim.ai/tools/microsoft_ad",
"operations": [
{
"name": "List Users",
"description": "List users in Azure AD (Microsoft Entra ID)"
},
{
"name": "Get User",
"description": "Get a user by ID or user principal name from Azure AD"
},
{
"name": "Create User",
"description": "Create a new user in Azure AD (Microsoft Entra ID)"
},
{
"name": "Update User",
"description": "Update user properties in Azure AD (Microsoft Entra ID)"
},
{
"name": "Delete User",
"description": "Delete a user from Azure AD (Microsoft Entra ID). The user is moved to a temporary container and can be restored within 30 days."
},
{
"name": "List Groups",
"description": "List groups in Azure AD (Microsoft Entra ID)"
},
{
"name": "Get Group",
"description": "Get a group by ID from Azure AD (Microsoft Entra ID)"
},
{
"name": "Create Group",
"description": "Create a new group in Azure AD (Microsoft Entra ID)"
},
{
"name": "Update Group",
"description": "Update group properties in Azure AD (Microsoft Entra ID)"
},
{
"name": "Delete Group",
"description": "Delete a group from Azure AD (Microsoft Entra ID). Microsoft 365 and security groups can be restored within 30 days."
},
{
"name": "List Group Members",
"description": "List members of a group in Azure AD (Microsoft Entra ID)"
},
{
"name": "Add Group Member",
"description": "Add a member to a group in Azure AD (Microsoft Entra ID)"
},
{
"name": "Remove Group Member",
"description": "Remove a member from a group in Azure AD (Microsoft Entra ID)"
}
],
"operationCount": 13,
"triggers": [],
"triggerCount": 0,
"authType": "oauth",
"category": "tools"
},
{
"type": "box",
"slug": "box",
@@ -1917,31 +1986,31 @@
"operations": [
{
"name": "Launch Agent",
"description": "Cursor API key"
"description": "Start a new cloud agent to work on a GitHub repository with the given instructions."
},
{
"name": "Add Follow-up",
"description": "Cursor API key"
"description": "Add a follow-up instruction to an existing cloud agent."
},
{
"name": "Get Agent Status",
"description": "Cursor API key"
"description": "Retrieve the current status and results of a cloud agent."
},
{
"name": "Get Conversation",
"description": "Cursor API key"
"description": "Retrieve the conversation history of a cloud agent, including all user prompts and assistant responses."
},
{
"name": "List Agents",
"description": "Cursor API key"
"description": "List all cloud agents for the authenticated user with optional pagination."
},
{
"name": "Stop Agent",
"description": "Cursor API key"
"description": "Stop a running cloud agent. This pauses the agent without deleting it."
},
{
"name": "Delete Agent",
"description": "Cursor API key"
"description": "Permanently delete a cloud agent. This action cannot be undone."
}
],
"operationCount": 7,
@@ -3881,7 +3950,7 @@
"operations": [
{
"name": "List Files",
"description": "List files and folders in Google Drive with complete metadata"
"description": "List Google Drive files"
},
{
"name": "Get File Info",
@@ -5267,6 +5336,43 @@
"authType": "api-key",
"category": "tools"
},
{
"type": "infisical",
"slug": "infisical",
"name": "Infisical",
"description": "Manage secrets with Infisical",
"longDescription": "Integrate Infisical into your workflow. List, get, create, update, and delete secrets across project environments.",
"bgColor": "#F7FE62",
"iconName": "InfisicalIcon",
"docsUrl": "https://docs.sim.ai/tools/infisical",
"operations": [
{
"name": "List Secrets",
"description": "List all secrets in a project environment. Returns secret keys, values, comments, tags, and metadata."
},
{
"name": "Get Secret",
"description": "Retrieve a single secret by name from a project environment."
},
{
"name": "Create Secret",
"description": "Create a new secret in a project environment."
},
{
"name": "Update Secret",
"description": "Update an existing secret in a project environment."
},
{
"name": "Delete Secret",
"description": "Delete a secret from a project environment."
}
],
"operationCount": 5,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
"category": "tools"
},
{
"type": "intercom_v2",
"slug": "intercom",
@@ -5279,63 +5385,63 @@
"operations": [
{
"name": "Create Contact",
"description": "Intercom API access token"
"description": "Create a new contact in Intercom with email, external_id, or role"
},
{
"name": "Get Contact",
"description": "Intercom API access token"
"description": "Get a single contact by ID from Intercom"
},
{
"name": "Update Contact",
"description": "Intercom API access token"
"description": "Update an existing contact in Intercom"
},
{
"name": "List Contacts",
"description": "Intercom API access token"
"description": "List all contacts from Intercom with pagination support"
},
{
"name": "Search Contacts",
"description": "Intercom API access token"
"description": "Search for contacts in Intercom using a query"
},
{
"name": "Delete Contact",
"description": "Intercom API access token"
"description": "Delete a contact from Intercom by ID"
},
{
"name": "Create Company",
"description": "Intercom API access token"
"description": "Create or update a company in Intercom"
},
{
"name": "Get Company",
"description": "Intercom API access token"
"description": "Retrieve a single company by ID from Intercom"
},
{
"name": "List Companies",
"description": "Intercom API access token"
"description": "List all companies from Intercom with pagination support. Note: This endpoint has a limit of 10,000 companies that can be returned using pagination. For datasets larger than 10,000 companies, use the Scroll API instead."
},
{
"name": "Get Conversation",
"description": "Intercom API access token"
"description": "Retrieve a single conversation by ID from Intercom"
},
{
"name": "List Conversations",
"description": "Intercom API access token"
"description": "List all conversations from Intercom with pagination support"
},
{
"name": "Reply to Conversation",
"description": "Intercom API access token"
"description": "Reply to a conversation as an admin in Intercom"
},
{
"name": "Search Conversations",
"description": "Intercom API access token"
"description": "Search for conversations in Intercom using a query"
},
{
"name": "Create Ticket",
"description": "Intercom API access token"
"description": "Create a new ticket in Intercom"
},
{
"name": "Get Ticket",
"description": "Intercom API access token"
"description": "Retrieve a single ticket by ID from Intercom"
},
{
"name": "Update Ticket",
@@ -5343,7 +5449,7 @@
},
{
"name": "Create Message",
"description": "Intercom API access token"
"description": "Create and send a new admin-initiated message in Intercom"
},
{
"name": "List Admins",
@@ -6850,15 +6956,15 @@
"operations": [
{
"name": "Add Memories",
"description": ""
"description": "Add memories to Mem0 for persistent storage and retrieval"
},
{
"name": "Search Memories",
"description": ""
"description": "Search for memories in Mem0 using semantic search"
},
{
"name": "Get Memories",
"description": ""
"description": "Retrieve memories from Mem0 by ID or filter criteria"
}
],
"operationCount": 3,
@@ -6883,7 +6989,7 @@
},
{
"name": "Get All Memories",
"description": ""
"description": "Retrieve all memories from the database"
},
{
"name": "Get Memory",
@@ -8210,7 +8316,7 @@
},
{
"name": "Unsave",
"description": ""
"description": "Remove a Reddit post or comment from your saved items"
},
{
"name": "Reply",
@@ -8388,7 +8494,7 @@
"operations": [
{
"name": "Send Email",
"description": ""
"description": "Send an email using your own Resend API key and from address"
},
{
"name": "Get Email",

View File

@@ -1,15 +0,0 @@
/**
* Utility helpers for the integrations landing pages.
* Shared across the listing grid, individual integration cards, and slug pages.
*/
/** bgColor values that are visually light and require dark icon text. */
const LIGHT_BG = new Set(['#e0e0e0', '#f5f5f5', '#ffffff', '#ececec', '#f0f0f0'])
/**
* Returns true when `bgColor` is a light color that requires dark foreground text.
* Handles gradient strings safely — they always use light foreground (white).
*/
export function isLightBg(bgColor: string): boolean {
return LIGHT_BG.has(bgColor.toLowerCase())
}

View File

@@ -1,7 +1,9 @@
import { getNavBlogPosts } from '@/lib/blog/registry'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
export default function IntegrationsLayout({ children }: { children: React.ReactNode }) {
export default async function IntegrationsLayout({ children }: { children: React.ReactNode }) {
const blogPosts = await getNavBlogPosts()
const orgJsonLd = {
'@context': 'https://schema.org',
'@type': 'Organization',
@@ -34,7 +36,7 @@ export default function IntegrationsLayout({ children }: { children: React.React
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
/>
<header>
<Navbar />
<Navbar blogPosts={blogPosts} />
</header>
<main className='relative flex-1'>{children}</main>
<Footer />

View File

@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import { IntegrationGrid } from './components/integration-grid'
import { RequestIntegrationModal } from './components/request-integration-modal'
import { blockTypeToIconMap } from './data/icon-mapping'
import integrations from './data/integrations.json'
import { POPULAR_WORKFLOWS } from './data/popular-workflows'
@@ -138,26 +139,7 @@ export default function IntegrationsPage() {
Let us know and we&apos;ll prioritize it.
</p>
</div>
<a
href='https://github.com/simstudioai/sim/issues/new?labels=integration+request&template=integration_request.md'
target='_blank'
rel='noopener noreferrer'
className='inline-flex h-[32px] shrink-0 items-center gap-[6px] rounded-[5px] border border-[#3d3d3d] px-[10px] font-[430] font-season text-[#ECECEC] text-[14px] transition-colors hover:bg-[#2A2A2A]'
>
Request an integration
<svg
aria-hidden='true'
className='h-3 w-3'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
<polyline points='15 3 21 3 21 9' />
<line x1='10' x2='21' y1='14' y2='3' />
</svg>
</a>
<RequestIntegrationModal />
</div>
</div>
</>

View File

@@ -1,19 +1,11 @@
'use client'
import { useEffect } from 'react'
import Link from 'next/link'
import { getEnv } from '@/lib/core/config/env'
import { LegalLayout } from '@/app/(landing)/components'
import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components'
export default function PrivacyPolicy() {
useEffect(() => {
const privacyUrl = getEnv('NEXT_PUBLIC_PRIVACY_URL')
if (privacyUrl?.startsWith('http')) {
window.location.href = privacyUrl
}
}, [])
return (
<LegalLayout title='Privacy Policy'>
<ExternalRedirect url={getEnv('NEXT_PUBLIC_PRIVACY_URL') ?? ''} />
<section>
<p className='mb-4'>Last Updated: October 11, 2025</p>
<p>

View File

@@ -1,19 +1,11 @@
'use client'
import { useEffect } from 'react'
import Link from 'next/link'
import { getEnv } from '@/lib/core/config/env'
import { LegalLayout } from '@/app/(landing)/components'
import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components'
export default function TermsOfService() {
useEffect(() => {
const termsUrl = getEnv('NEXT_PUBLIC_TERMS_URL')
if (termsUrl?.startsWith('http')) {
window.location.href = termsUrl
}
}, [])
return (
<LegalLayout title='Terms of Service'>
<ExternalRedirect url={getEnv('NEXT_PUBLIC_TERMS_URL') ?? ''} />
<section>
<p className='mb-4'>Last Updated: October 11, 2025</p>
<p>

View File

@@ -0,0 +1,110 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { env } from '@/lib/core/config/env'
import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { generateRequestId } from '@/lib/core/utils/request'
import { getEmailDomain } from '@/lib/core/utils/urls'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
const logger = createLogger('IntegrationRequestAPI')
const rateLimiter = new RateLimiter()
const PUBLIC_ENDPOINT_RATE_LIMIT: TokenBucketConfig = {
maxTokens: 10,
refillRate: 5,
refillIntervalMs: 60_000,
}
const integrationRequestSchema = z.object({
integrationName: z.string().min(1, 'Integration name is required').max(200),
email: z.string().email('A valid email is required'),
useCase: z.string().max(2000).optional(),
})
export async function POST(req: NextRequest) {
const requestId = generateRequestId()
try {
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
const storageKey = `public:integration-request:${ip}`
const { allowed, remaining, resetAt } = await rateLimiter.checkRateLimitDirect(
storageKey,
PUBLIC_ENDPOINT_RATE_LIMIT
)
if (!allowed) {
logger.warn(`[${requestId}] Rate limit exceeded for IP ${ip}`, { remaining, resetAt })
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{
status: 429,
headers: { 'Retry-After': String(Math.ceil((resetAt.getTime() - Date.now()) / 1000)) },
}
)
}
const body = await req.json()
const validationResult = integrationRequestSchema.safeParse(body)
if (!validationResult.success) {
logger.warn(`[${requestId}] Invalid integration request data`, {
errors: validationResult.error.format(),
})
return NextResponse.json(
{ error: 'Invalid request data', details: validationResult.error.format() },
{ status: 400 }
)
}
const { integrationName, email, useCase } = validationResult.data
logger.info(`[${requestId}] Processing integration request`, {
integrationName,
email: `${email.substring(0, 3)}***`,
})
const emailText = `Integration: ${integrationName}
From: ${email}
Submitted: ${new Date().toISOString()}
${useCase ? `Use Case:\n${useCase}` : 'No use case provided.'}
`
const emailResult = await sendEmail({
to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`],
subject: `[INTEGRATION REQUEST] ${integrationName}`,
text: emailText,
from: getFromEmailAddress(),
replyTo: email,
emailType: 'transactional',
})
if (!emailResult.success) {
logger.error(`[${requestId}] Error sending integration request email`, emailResult.message)
return NextResponse.json({ error: 'Failed to send request' }, { status: 500 })
}
logger.info(`[${requestId}] Integration request email sent successfully`)
return NextResponse.json(
{ success: true, message: 'Integration request submitted successfully' },
{ status: 200 }
)
} catch (error) {
if (error instanceof Error && error.message.includes('not configured')) {
logger.error(`[${requestId}] Email service configuration error`, error)
return NextResponse.json(
{ error: 'Email service is temporarily unavailable. Please try again later.' },
{ status: 500 }
)
}
logger.error(`[${requestId}] Error processing integration request`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -1,14 +1,16 @@
import { getNavBlogPosts } from '@/lib/blog/registry'
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
export default function ChangelogLayout({ children }: { children: React.ReactNode }) {
export default async function ChangelogLayout({ children }: { children: React.ReactNode }) {
const blogPosts = await getNavBlogPosts()
return (
<div
className={`${martianMono.variable} relative min-h-screen bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]`}
>
<header>
<Navbar />
<Navbar blogPosts={blogPosts} />
</header>
{children}
<Footer hideCTA />

View File

@@ -1,16 +1,18 @@
import Link from 'next/link'
import { getNavBlogPosts } from '@/lib/blog/registry'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Navbar from '@/app/(home)/components/navbar/navbar'
const CTA_BASE =
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
export default function NotFound() {
export default async function NotFound() {
const blogPosts = await getNavBlogPosts()
return (
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
<header className='shrink-0 bg-[#1C1C1C]'>
<Navbar />
<Navbar blogPosts={blogPosts} />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='flex flex-col items-center gap-[12px]'>

View File

@@ -4140,7 +4140,7 @@ export function IncidentioIcon(props: SVGProps<SVGSVGElement>) {
export function InfisicalIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 273 182' xmlns='http://www.w3.org/2000/svg'>
<svg {...props} viewBox='20 25 233 132' xmlns='http://www.w3.org/2000/svg'>
<path
d='m191.6 39.4c-20.3 0-37.15 13.21-52.9 30.61-12.99-16.4-29.8-30.61-51.06-30.61-27.74 0-50.44 23.86-50.44 51.33 0 26.68 21.43 51.8 48.98 51.8 20.55 0 37.07-13.86 51.32-31.81 12.69 16.97 29.1 31.41 53.2 31.41 27.13 0 49.85-22.96 49.85-51.4 0-27.12-20.44-51.33-48.95-51.33zm-104.3 77.94c-14.56 0-25.51-12.84-25.51-26.07 0-13.7 10.95-28.29 25.51-28.29 14.93 0 25.71 11.6 37.6 27.34-11.31 15.21-22.23 27.02-37.6 27.02zm104.4 0.25c-15 0-25.28-11.13-37.97-27.37 12.69-16.4 22.01-27.24 37.59-27.24 14.97 0 24.79 13.25 24.79 27.26 0 13-10.17 27.35-24.41 27.35z'
fill='currentColor'
@@ -4253,7 +4253,7 @@ export function ZoomIcon(props: SVGProps<SVGSVGElement>) {
fill='currentColor'
width='800px'
height='800px'
viewBox='0 0 32 32'
viewBox='-1 9.5 34 13'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
>

View File

@@ -1,5 +1,6 @@
import fs from 'fs/promises'
import path from 'path'
import { cache } from 'react'
import matter from 'gray-matter'
import { compileMDX } from 'next-mdx-remote/rsc'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
@@ -90,6 +91,20 @@ export async function getAllPostMeta(): Promise<BlogMeta[]> {
return (await scanFrontmatters()).filter((p) => !p.draft)
}
export const getNavBlogPosts = cache(
async (): Promise<Pick<BlogMeta, 'slug' | 'title' | 'ogImage'>[]> => {
const allPosts = await getAllPostMeta()
const featuredPost = allPosts.find((p) => p.featured) ?? allPosts[0]
if (!featuredPost) return []
const recentPosts = allPosts.filter((p) => p.slug !== featuredPost.slug).slice(0, 4)
return [featuredPost, ...recentPosts].map((p) => ({
slug: p.slug,
title: p.title,
ogImage: p.ogImage,
}))
}
)
export async function getAllTags(): Promise<TagWithCount[]> {
const posts = await getAllPostMeta()
const counts: Record<string, number> = {}

View File

@@ -164,6 +164,34 @@ export class RateLimiter {
}
}
async checkRateLimitDirect(
storageKey: string,
config: { maxTokens: number; refillRate: number; refillIntervalMs: number }
): Promise<RateLimitResult> {
try {
const result = await this.storage.consumeTokens(storageKey, 1, config)
if (!result.allowed) {
logger.info('Rate limit exceeded', { storageKey, tokensRemaining: result.tokensRemaining })
}
return {
allowed: result.allowed,
remaining: result.tokensRemaining,
resetAt: result.resetAt,
retryAfterMs: result.retryAfterMs,
}
} catch (error) {
logger.error('Rate limit storage error - failing open (allowing request)', {
error: error instanceof Error ? error.message : String(error),
storageKey,
})
return {
allowed: true,
remaining: 1,
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
}
}
}
async resetRateLimit(rateLimitKey: string): Promise<void> {
try {
await Promise.all([

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -317,26 +317,48 @@ function extractOperationsFromContent(blockContent: string): { label: string; id
* Scan all tool files under apps/sim/tools/ and build a map from tool ID to description.
* Used to enrich operation entries with descriptions.
*/
async function buildToolDescriptionMap(): Promise<Map<string, string>> {
interface ToolMaps {
desc: Map<string, string>
name: Map<string, string>
}
async function buildToolDescriptionMap(): Promise<ToolMaps> {
const toolsDir = path.join(rootDir, 'apps/sim/tools')
const map = new Map<string, string>()
const desc = new Map<string, string>()
const name = new Map<string, string>()
try {
const toolFiles = await glob(`${toolsDir}/**/*.ts`)
for (const file of toolFiles) {
if (file.endsWith('index.ts') || file.endsWith('types.ts')) continue
const content = fs.readFileSync(file, 'utf-8')
// Match top-level id + description fields in ToolConfig objects
const idMatches = [...content.matchAll(/\bid\s*:\s*['"]([^'"]+)['"]/g)]
const descMatches = [...content.matchAll(/\bdescription\s*:\s*['"]([^'"]{5,})['"]/g)]
if (idMatches.length > 0 && descMatches.length > 0) {
// The first id match and first description match are the tool's own fields
map.set(idMatches[0][1], descMatches[0][1])
// Find every `id: 'tool_id'` occurrence in the file. For each, search
// the next ~600 characters for `name:` and `description:` fields, cutting
// off at the first `params:` block within that window. This handles both
// the simple inline pattern (id → description → params in one object) and
// the two-step pattern (base object holds params, ToolConfig export holds
// id + description after the base object).
const idRegex = /\bid\s*:\s*['"]([^'"]+)['"]/g
let idMatch: RegExpExecArray | null
while ((idMatch = idRegex.exec(content)) !== null) {
const toolId = idMatch[1]
if (desc.has(toolId)) continue
const windowStart = idMatch.index
const windowEnd = Math.min(windowStart + 600, content.length)
const window = content.substring(windowStart, windowEnd)
// Stop before any params block so we don't pick up param-level values
const paramsOffset = window.search(/\bparams\s*:\s*\{/)
const searchWindow = paramsOffset > 0 ? window.substring(0, paramsOffset) : window
const descMatch = searchWindow.match(/\bdescription\s*:\s*['"]([^'"]{5,})['"]/)
const nameMatch = searchWindow.match(/\bname\s*:\s*['"]([^'"]+)['"]/)
if (descMatch) desc.set(toolId, descMatch[1])
if (nameMatch) name.set(toolId, nameMatch[1])
}
}
} catch {
// Non-fatal: descriptions will be empty strings
}
return map
return { desc, name }
}
/**
@@ -481,7 +503,7 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): Promi
}
const triggerRegistry = await buildTriggerRegistry()
const toolDescMap = await buildToolDescriptionMap()
const { desc: toolDescMap, name: toolNameMap } = await buildToolDescriptionMap()
const integrations: IntegrationEntry[] = []
const seenBaseTypes = new Set<string>()
const blockFiles = (await glob(`${BLOCKS_PATH}/*.ts`)).sort()
@@ -517,11 +539,27 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): Promi
const iconName = (config as any).iconName || iconMapping[blockType] || ''
const rawOps: { label: string; id: string }[] = (config as any).operations || []
// Enrich each operation with a description from the tool registry
// Enrich each operation with a description from the tool registry.
// Primary lookup: derive toolId as `{baseType}_{operationId}` and check
// the map directly. Fallback: some blocks use short op IDs that don't
// match tool IDs (e.g. Slack uses "send" while the tool ID is
// "slack_message"). In that case, find the tool in tools.access whose
// name exactly matches the operation label.
const toolsAccess: string[] = (config as any).tools?.access || []
const operations: OperationInfo[] = rawOps.map(({ label, id }) => {
const toolId = `${baseType}_${id}`
const desc = toolDescMap.get(toolId) || toolDescMap.get(id) || ''
return { name: label, description: desc }
let opDesc = toolDescMap.get(toolId) || toolDescMap.get(id) || ''
if (!opDesc && toolsAccess.length > 0) {
for (const tId of toolsAccess) {
if (toolNameMap.get(tId)?.toLowerCase() === label.toLowerCase()) {
opDesc = toolDescMap.get(tId) || ''
if (opDesc) break
}
}
}
return { name: label, description: opDesc }
})
const triggerIds: string[] = (config as any).triggerIds || []