Compare commits

..

33 Commits

Author SHA1 Message Date
Vikhyath Mondreti
45371e521e v0.5.66: external http requests fix, ring highlighting 2026-01-21 02:55:39 -08:00
Waleed
0ce0f98aa5 v0.5.65: gemini updates, textract integration, ui updates (#2909)
* fix(google): wrap primitive tool responses for Gemini API compatibility (#2900)

* fix(canonical): copilot path + update parent (#2901)

* fix(rss): add top-level title, link, pubDate fields to RSS trigger output (#2902)

* fix(rss): add top-level title, link, pubDate fields to RSS trigger output

* fix(imap): add top-level fields to IMAP trigger output

* improvement(browseruse): add profile id param (#2903)

* improvement(browseruse): add profile id param

* make request a stub since we have directExec

* improvement(executor): upgraded abort controller to handle aborts for loops and parallels (#2880)

* improvement(executor): upgraded abort controller to handle aborts for loops and parallels

* comments

* improvement(files): update execution for passing base64 strings (#2906)

* progress

* improvement(execution): update execution for passing base64 strings

* fix types

* cleanup comments

* path security vuln

* reject promise correctly

* fix redirect case

* remove proxy routes

* fix tests

* use ipaddr

* feat(tools): added textract, added v2 for mistral, updated tag dropdown (#2904)

* feat(tools): added textract

* cleanup

* ack pr comments

* reorder

* removed upload for textract async version

* fix additional fields dropdown in editor, update parser to leave validation to be done on the server

* added mistral v2, files v2, and finalized textract

* updated the rest of the old file patterns, updated mistral outputs for v2

* updated tag dropdown to parse non-operation fields as well

* updated extension finder

* cleanup

* added description for inputs to workflow

* use helper for internal route check

* fix tag dropdown merge conflict change

* remove duplicate code

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>

* fix(ui): change add inputs button to match output selector (#2907)

* fix(canvas): removed invite to workspace from canvas popover (#2908)

* fix(canvas): removed invite to workspace

* removed unused props

* fix(copilot): legacy tool display names (#2911)

* fix(a2a): canonical merge  (#2912)

* fix canonical merge

* fix empty array case

* fix(change-detection): copilot diffs have extra field (#2913)

* improvement(logs): improved logs ui bugs, added subflow disable UI (#2910)

* improvement(logs): improved logs ui bugs, added subflow disable UI

* added duplicate to action bar for subflows

* feat(broadcast): email v0.5 (#2905)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
2026-01-20 23:54:55 -08:00
Waleed
dff1c9d083 v0.5.64: unsubscribe, search improvements, metrics, additional SSO configuration 2026-01-20 00:34:11 -08:00
Vikhyath Mondreti
b09f683072 v0.5.63: ui and performance improvements, more google tools 2026-01-18 15:22:42 -08:00
Vikhyath Mondreti
a8bb0db660 v0.5.62: webhook bug fixes, seeding default subblock values, block selection fixes 2026-01-16 20:27:06 -08:00
Waleed
af82820a28 v0.5.61: webhook improvements, workflow controls, react query for deployment status, chat fixes, reducto and pulse OCR, linear fixes 2026-01-16 18:06:23 -08:00
Waleed
4372841797 v0.5.60: invitation flow improvements, chat fixes, a2a improvements, additional copilot actions 2026-01-15 00:02:18 -08:00
Waleed
5e8c843241 v0.5.59: a2a support, documentation 2026-01-13 13:21:21 -08:00
Waleed
7bf3d73ee6 v0.5.58: export folders, new tools, permissions groups enhancements 2026-01-13 00:56:59 -08:00
Vikhyath Mondreti
7ffc11a738 v0.5.57: subagents, context menu improvements, bug fixes 2026-01-11 11:38:40 -08:00
Waleed
be578e2ed7 v0.5.56: batch operations, access control and permission groups, billing fixes 2026-01-10 00:31:34 -08:00
Waleed
f415e5edc4 v0.5.55: polling groups, bedrock provider, devcontainer fixes, workflow preview enhancements 2026-01-08 23:36:56 -08:00
Waleed
13a6e6c3fa v0.5.54: seo, model blacklist, helm chart updates, fireflies integration, autoconnect improvements, billing fixes 2026-01-07 16:09:45 -08:00
Waleed
f5ab7f21ae v0.5.53: hotkey improvements, added redis fallback, fixes for workflow tool 2026-01-06 23:34:52 -08:00
Waleed
bfb6fffe38 v0.5.52: new port-based router block, combobox expression and variable support 2026-01-06 16:14:10 -08:00
Waleed
4fbec0a43f v0.5.51: triggers, kb, condition block improvements, supabase and grain integration updates 2026-01-06 14:26:46 -08:00
Waleed
585f5e365b v0.5.50: import improvements, ui upgrades, kb styling and performance improvements 2026-01-05 00:35:55 -08:00
Waleed
3792bdd252 v0.5.49: hitl improvements, new email styles, imap trigger, logs context menu (#2672)
* feat(logs-context-menu): consolidated logs utils and types, added logs record context menu (#2659)

* feat(email): welcome email; improvement(emails): ui/ux (#2658)

* feat(email): welcome email; improvement(emails): ui/ux

* improvement(emails): links, accounts, preview

* refactor(emails): file structure and wrapper components

* added envvar for personal emails sent, added isHosted gate

* fixed failing tests, added env mock

* fix: removed comment

---------

Co-authored-by: waleed <walif6@gmail.com>

* fix(logging): hitl + trigger dev crash protection (#2664)

* hitl gaps

* deal with trigger worker crashes

* cleanup import strcuture

* feat(imap): added support for imap trigger (#2663)

* feat(tools): added support for imap trigger

* feat(imap): added parity, tested

* ack PR comments

* final cleanup

* feat(i18n): update translations (#2665)

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* fix(grain): updated grain trigger to auto-establish trigger (#2666)

Co-authored-by: aadamgough <adam@sim.ai>

* feat(admin): routes to manage deployments (#2667)

* feat(admin): routes to manage deployments

* fix naming fo deployed by

* feat(time-picker): added timepicker emcn component, added to playground, added searchable prop for dropdown, added more timezones for schedule, updated license and notice date (#2668)

* feat(time-picker): added timepicker emcn component, added to playground, added searchable prop for dropdown, added more timezones for schedule, updated license and notice date

* removed unused params, cleaned up redundant utils

* improvement(invite): aligned styling (#2669)

* improvement(invite): aligned with rest of app

* fix(invite): error handling

* fix: addressed comments

---------

Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
Co-authored-by: aadamgough <adam@sim.ai>
2026-01-03 13:19:18 -08:00
Waleed
eb5d1f3e5b v0.5.48: copy-paste workflow blocks, docs updates, mcp tool fixes 2025-12-31 18:00:04 -08:00
Waleed
54ab82c8dd v0.5.47: deploy workflow as mcp, kb chunks tokenizer, UI improvements, jira service management tools 2025-12-30 23:18:58 -08:00
Waleed
f895bf469b v0.5.46: build improvements, greptile, light mode improvements 2025-12-29 02:17:52 -08:00
Waleed
dd3209af06 v0.5.45: light mode fixes, realtime usage indicator, docker build improvements 2025-12-27 19:57:42 -08:00
Waleed
b6ba3b50a7 v0.5.44: keyboard shortcuts, autolayout, light mode, byok, testing improvements 2025-12-26 21:25:19 -08:00
Waleed
b304233062 v0.5.43: export logs, circleback, grain, vertex, code hygiene, schedule improvements 2025-12-23 19:19:18 -08:00
Vikhyath Mondreti
57e4b49bd6 v0.5.42: fix memory migration 2025-12-23 01:24:54 -08:00
Vikhyath Mondreti
e12dd204ed v0.5.41: memory fixes, copilot improvements, knowledgebase improvements, LLM providers standardization 2025-12-23 00:15:18 -08:00
Vikhyath Mondreti
3d9d9cbc54 v0.5.40: supabase ops to allow non-public schemas, jira uuid 2025-12-21 22:28:05 -08:00
Waleed
0f4ec962ad v0.5.39: notion, workflow variables fixes 2025-12-20 20:44:00 -08:00
Waleed
4827866f9a v0.5.38: snap to grid, copilot ux improvements, billing line items 2025-12-20 17:24:38 -08:00
Waleed
3e697d9ed9 v0.5.37: redaction utils consolidation, logs updates, autoconnect improvements, additional kb tag types 2025-12-19 22:31:55 -08:00
Martin Yankov
4431a1a484 fix(helm): add custom egress rules to realtime network policy (#2481)
The realtime service network policy was missing the custom egress rules section
that allows configuration of additional egress rules via values.yaml. This caused
the realtime pods to be unable to connect to external databases (e.g., PostgreSQL
on port 5432) when using external database configurations.

The app network policy already had this section, but the realtime network policy
was missing it, creating an inconsistency and preventing the realtime service
from accessing external databases configured via networkPolicy.egress values.

This fix adds the same custom egress rules template section to the realtime
network policy, matching the app network policy behavior and allowing users to
configure database connectivity via values.yaml.
2025-12-19 18:59:08 -08:00
Waleed
4d1a9a3f22 v0.5.36: hitl improvements, opengraph, slack fixes, one-click unsubscribe, auth checks, new db indexes 2025-12-19 01:27:49 -08:00
Vikhyath Mondreti
eb07a080fb v0.5.35: helm updates, copilot improvements, 404 for docs, salesforce fixes, subflow resize clamping 2025-12-18 16:23:19 -08:00
362 changed files with 5906 additions and 21024 deletions

View File

@@ -124,44 +124,11 @@ Choose between four types of loops:
3. Drag other blocks inside the loop container
4. Connect the blocks as needed
### Referencing Loop Data
### Accessing Results
There's an important distinction between referencing loop data from **inside** vs **outside** the loop:
After a loop completes, you can access aggregated results:
<Tabs items={['Inside the Loop', 'Outside the Loop']}>
<Tab>
**Inside the loop**, use `<loop.>` references to access the current iteration context:
- **`<loop.index>`**: Current iteration number (0-based)
- **`<loop.currentItem>`**: Current item being processed (forEach only)
- **`<loop.items>`**: Full collection being iterated (forEach only)
```
// Inside a Function block within the loop
const idx = <loop.index>; // 0, 1, 2, ...
const item = <loop.currentItem>; // Current item
```
<Callout type="info">
These references are only available for blocks **inside** the loop container. They give you access to the current iteration's context.
</Callout>
</Tab>
<Tab>
**Outside the loop** (after it completes), reference the loop block by its name to access aggregated results:
- **`<LoopBlockName.results>`**: Array of results from all iterations
```
// If your loop block is named "Process Items"
const allResults = <processitems.results>;
// Returns: [result1, result2, result3, ...]
```
<Callout type="info">
After the loop completes, use the loop's block name (not `loop.`) to access the collected results. The block name is normalized (lowercase, no spaces).
</Callout>
</Tab>
</Tabs>
- **`<loop.results>`**: Array of results from all loop iterations
## Example Use Cases
@@ -217,29 +184,28 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
</ul>
</Tab>
<Tab>
Available **inside** the loop only:
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<loop.index>"}</strong>: Current iteration number (0-based)
<strong>loop.currentItem</strong>: Current item being processed
</li>
<li>
<strong>{"<loop.currentItem>"}</strong>: Current item being processed (forEach only)
<strong>loop.index</strong>: Current iteration number (0-based)
</li>
<li>
<strong>{"<loop.items>"}</strong>: Full collection (forEach only)
<strong>loop.items</strong>: Full collection (forEach loops)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<blockname.results>"}</strong>: Array of all iteration results (accessed via block name)
<strong>loop.results</strong>: Array of all iteration results
</li>
<li>
<strong>Structure</strong>: Results maintain iteration order
</li>
<li>
<strong>Access</strong>: Available in blocks after the loop completes
<strong>Access</strong>: Available in blocks after the loop
</li>
</ul>
</Tab>

View File

@@ -76,44 +76,11 @@ Choose between two types of parallel execution:
3. Drag a single block inside the parallel container
4. Connect the block as needed
### Referencing Parallel Data
### Accessing Results
There's an important distinction between referencing parallel data from **inside** vs **outside** the parallel block:
After a parallel block completes, you can access aggregated results:
<Tabs items={['Inside the Parallel', 'Outside the Parallel']}>
<Tab>
**Inside the parallel**, use `<parallel.>` references to access the current instance context:
- **`<parallel.index>`**: Current instance number (0-based)
- **`<parallel.currentItem>`**: Item for this instance (collection-based only)
- **`<parallel.items>`**: Full collection being distributed (collection-based only)
```
// Inside a Function block within the parallel
const idx = <parallel.index>; // 0, 1, 2, ...
const item = <parallel.currentItem>; // This instance's item
```
<Callout type="info">
These references are only available for blocks **inside** the parallel container. They give you access to the current instance's context.
</Callout>
</Tab>
<Tab>
**Outside the parallel** (after it completes), reference the parallel block by its name to access aggregated results:
- **`<ParallelBlockName.results>`**: Array of results from all instances
```
// If your parallel block is named "Process Tasks"
const allResults = <processtasks.results>;
// Returns: [result1, result2, result3, ...]
```
<Callout type="info">
After the parallel completes, use the parallel's block name (not `parallel.`) to access the collected results. The block name is normalized (lowercase, no spaces).
</Callout>
</Tab>
</Tabs>
- **`<parallel.results>`**: Array of results from all parallel instances
## Example Use Cases
@@ -131,11 +98,11 @@ Parallel (["gpt-4o", "claude-3.7-sonnet", "gemini-2.5-pro"]) → Agent → Evalu
### Result Aggregation
Results from all parallel instances are automatically collected and accessible via the block name:
Results from all parallel instances are automatically collected:
```javascript
// In a Function block after a parallel named "Process Tasks"
const allResults = <processtasks.results>;
// In a Function block after the parallel
const allResults = input.parallel.results;
// Returns: [result1, result2, result3, ...]
```
@@ -191,26 +158,25 @@ Understanding when to use each:
</ul>
</Tab>
<Tab>
Available **inside** the parallel only:
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<parallel.index>"}</strong>: Instance number (0-based)
<strong>parallel.currentItem</strong>: Item for this instance
</li>
<li>
<strong>{"<parallel.currentItem>"}</strong>: Item for this instance (collection-based only)
<strong>parallel.index</strong>: Instance number (0-based)
</li>
<li>
<strong>{"<parallel.items>"}</strong>: Full collection (collection-based only)
<strong>parallel.items</strong>: Full collection (collection-based)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<blockname.results>"}</strong>: Array of all instance results (accessed via block name)
<strong>parallel.results</strong>: Array of all instance results
</li>
<li>
<strong>Access</strong>: Available in blocks after the parallel completes
<strong>Access</strong>: Available in blocks after the parallel
</li>
</ul>
</Tab>

View File

@@ -2,9 +2,10 @@
import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
@@ -21,10 +22,8 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
const logger = createLogger('LoginForm')
@@ -106,7 +105,8 @@ export default function LoginPage({
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const buttonClass = useBrandedButtonClass()
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const [isButtonHovered, setIsButtonHovered] = useState(false)
const [callbackUrl, setCallbackUrl] = useState('/workspace')
const [isInviteFlow, setIsInviteFlow] = useState(false)
@@ -114,6 +114,7 @@ export default function LoginPage({
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
const [isSubmittingReset, setIsSubmittingReset] = useState(false)
const [isResetButtonHovered, setIsResetButtonHovered] = useState(false)
const [resetStatus, setResetStatus] = useState<{
type: 'success' | 'error' | null
message: string
@@ -122,7 +123,6 @@ export default function LoginPage({
const [email, setEmail] = useState('')
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(null)
useEffect(() => {
setMounted(true)
@@ -139,12 +139,32 @@ export default function LoginPage({
const inviteFlow = searchParams.get('invite_flow') === 'true'
setIsInviteFlow(inviteFlow)
}
const resetSuccess = searchParams.get('resetSuccess') === 'true'
if (resetSuccess) {
setResetSuccessMessage('Password reset successful. Please sign in with your new password.')
const checkCustomBrand = () => {
const computedStyle = getComputedStyle(document.documentElement)
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
if (brandAccent && brandAccent !== '#6f3dfa') {
setButtonClass('branded-button-custom')
} else {
setButtonClass('branded-button-gradient')
}
}
checkCustomBrand()
window.addEventListener('resize', checkCustomBrand)
const observer = new MutationObserver(checkCustomBrand)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style', 'class'],
})
return () => {
window.removeEventListener('resize', checkCustomBrand)
observer.disconnect()
}
}, [searchParams])
useEffect(() => {
@@ -182,13 +202,6 @@ export default function LoginPage({
e.preventDefault()
setIsLoading(true)
const redirectToVerify = (emailToVerify: string) => {
if (typeof window !== 'undefined') {
sessionStorage.setItem('verificationEmail', emailToVerify)
}
router.push('/verify')
}
const formData = new FormData(e.currentTarget)
const emailRaw = formData.get('email') as string
const email = emailRaw.trim().toLowerCase()
@@ -208,7 +221,6 @@ export default function LoginPage({
try {
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
let errorHandled = false
const result = await client.signIn.email(
{
@@ -219,16 +231,11 @@ export default function LoginPage({
{
onError: (ctx) => {
logger.error('Login error:', ctx.error)
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
errorHandled = true
redirectToVerify(email)
return
}
errorHandled = true
const errorMessage: string[] = ['Invalid email or password']
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
return
}
if (
ctx.error.code?.includes('BAD_REQUEST') ||
ctx.error.message?.includes('Email and password sign in is not enabled')
@@ -264,7 +271,6 @@ export default function LoginPage({
errorMessage.push('Too many requests. Please wait a moment before trying again.')
}
setResetSuccessMessage(null)
setPasswordErrors(errorMessage)
setShowValidationError(true)
},
@@ -272,25 +278,15 @@ export default function LoginPage({
)
if (!result || result.error) {
// Show error if not already handled by onError callback
if (!errorHandled) {
setResetSuccessMessage(null)
const errorMessage = result?.error?.message || 'Login failed. Please try again.'
setPasswordErrors([errorMessage])
setShowValidationError(true)
}
setIsLoading(false)
return
}
// Clear reset success message on successful login
setResetSuccessMessage(null)
// Explicit redirect fallback if better-auth doesn't redirect
router.push(safeCallbackUrl)
} catch (err: any) {
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
redirectToVerify(email)
if (typeof window !== 'undefined') {
sessionStorage.setItem('verificationEmail', email)
}
router.push('/verify')
return
}
@@ -404,13 +400,6 @@ export default function LoginPage({
</div>
)}
{/* Password reset success message */}
{resetSuccessMessage && (
<div className={`${inter.className} mt-1 space-y-1 text-[#4CAF50] text-xs`}>
<p>{resetSuccessMessage}</p>
</div>
)}
{/* Email/Password Form - show unless explicitly disabled */}
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
@@ -493,14 +482,24 @@ export default function LoginPage({
</div>
</div>
<BrandedButton
<Button
type='submit'
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
disabled={isLoading}
loading={isLoading}
loadingText='Signing in'
>
Sign in
</BrandedButton>
<span className='flex items-center gap-1'>
{isLoading ? 'Signing in...' : 'Sign in'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
</form>
)}
@@ -611,15 +610,25 @@ export default function LoginPage({
<p>{resetStatus.message}</p>
</div>
)}
<BrandedButton
<Button
type='button'
onClick={handleForgotPassword}
onMouseEnter={() => setIsResetButtonHovered(true)}
onMouseLeave={() => setIsResetButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
disabled={isSubmittingReset}
loading={isSubmittingReset}
loadingText='Sending'
>
Send Reset Link
</BrandedButton>
<span className='flex items-center gap-1'>
{isSubmittingReset ? 'Sending...' : 'Send Reset Link'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isResetButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
</div>
</DialogContent>
</Dialog>

View File

@@ -1,12 +1,12 @@
'use client'
import { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
interface RequestResetFormProps {
email: string
@@ -27,6 +27,36 @@ export function RequestResetForm({
statusMessage,
className,
}: RequestResetFormProps) {
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const [isButtonHovered, setIsButtonHovered] = useState(false)
useEffect(() => {
const checkCustomBrand = () => {
const computedStyle = getComputedStyle(document.documentElement)
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
if (brandAccent && brandAccent !== '#6f3dfa') {
setButtonClass('branded-button-custom')
} else {
setButtonClass('branded-button-gradient')
}
}
checkCustomBrand()
window.addEventListener('resize', checkCustomBrand)
const observer = new MutationObserver(checkCustomBrand)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style', 'class'],
})
return () => {
window.removeEventListener('resize', checkCustomBrand)
observer.disconnect()
}
}, [])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
onSubmit(email)
@@ -64,14 +94,24 @@ export function RequestResetForm({
)}
</div>
<BrandedButton
<Button
type='submit'
disabled={isSubmitting}
loading={isSubmitting}
loadingText='Sending'
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
>
Send Reset Link
</BrandedButton>
<span className='flex items-center gap-1'>
{isSubmitting ? 'Sending...' : 'Send Reset Link'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
</form>
)
}
@@ -98,6 +138,35 @@ export function SetNewPasswordForm({
const [validationMessage, setValidationMessage] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const [isButtonHovered, setIsButtonHovered] = useState(false)
useEffect(() => {
const checkCustomBrand = () => {
const computedStyle = getComputedStyle(document.documentElement)
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
if (brandAccent && brandAccent !== '#6f3dfa') {
setButtonClass('branded-button-custom')
} else {
setButtonClass('branded-button-gradient')
}
}
checkCustomBrand()
window.addEventListener('resize', checkCustomBrand)
const observer = new MutationObserver(checkCustomBrand)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style', 'class'],
})
return () => {
window.removeEventListener('resize', checkCustomBrand)
observer.disconnect()
}
}, [])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -227,14 +296,24 @@ export function SetNewPasswordForm({
)}
</div>
<BrandedButton
type='submit'
<Button
disabled={isSubmitting || !token}
loading={isSubmitting}
loadingText='Resetting'
type='submit'
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
>
Reset Password
</BrandedButton>
<span className='flex items-center gap-1'>
{isSubmitting ? 'Resetting...' : 'Reset Password'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
</form>
)
}

View File

@@ -2,9 +2,10 @@
import { Suspense, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { client, useSession } from '@/lib/auth/auth-client'
@@ -13,10 +14,8 @@ import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
const logger = createLogger('SignupForm')
@@ -96,7 +95,8 @@ function SignupFormContent({
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const [redirectUrl, setRedirectUrl] = useState('')
const [isInviteFlow, setIsInviteFlow] = useState(false)
const buttonClass = useBrandedButtonClass()
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const [isButtonHovered, setIsButtonHovered] = useState(false)
const [name, setName] = useState('')
const [nameErrors, setNameErrors] = useState<string[]>([])
@@ -126,6 +126,31 @@ function SignupFormContent({
if (inviteFlowParam === 'true') {
setIsInviteFlow(true)
}
const checkCustomBrand = () => {
const computedStyle = getComputedStyle(document.documentElement)
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
if (brandAccent && brandAccent !== '#6f3dfa') {
setButtonClass('branded-button-custom')
} else {
setButtonClass('branded-button-gradient')
}
}
checkCustomBrand()
window.addEventListener('resize', checkCustomBrand)
const observer = new MutationObserver(checkCustomBrand)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style', 'class'],
})
return () => {
window.removeEventListener('resize', checkCustomBrand)
observer.disconnect()
}
}, [searchParams])
const validatePassword = (passwordValue: string): string[] => {
@@ -475,14 +500,24 @@ function SignupFormContent({
</div>
</div>
<BrandedButton
<Button
type='submit'
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
disabled={isLoading}
loading={isLoading}
loadingText='Creating account'
>
Create account
</BrandedButton>
<span className='flex items-center gap-1'>
{isLoading ? 'Creating account' : 'Create account'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
</form>
)}

View File

@@ -13,7 +13,6 @@ import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
const logger = createLogger('SSOForm')
@@ -58,7 +57,7 @@ export default function SSOForm() {
const [email, setEmail] = useState('')
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const buttonClass = useBrandedButtonClass()
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const [callbackUrl, setCallbackUrl] = useState('/workspace')
useEffect(() => {
@@ -91,6 +90,31 @@ export default function SSOForm() {
setShowEmailValidationError(true)
}
}
const checkCustomBrand = () => {
const computedStyle = getComputedStyle(document.documentElement)
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
if (brandAccent && brandAccent !== '#6f3dfa') {
setButtonClass('branded-button-custom')
} else {
setButtonClass('branded-button-gradient')
}
}
checkCustomBrand()
window.addEventListener('resize', checkCustomBrand)
const observer = new MutationObserver(checkCustomBrand)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style', 'class'],
})
return () => {
window.removeEventListener('resize', checkCustomBrand)
observer.disconnect()
}
}, [searchParams])
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {

View File

@@ -8,7 +8,6 @@ import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { useVerification } from '@/app/(auth)/verify/use-verification'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
interface VerifyContentProps {
hasEmailService: boolean
@@ -59,7 +58,34 @@ function VerificationForm({
setCountdown(30)
}
const buttonClass = useBrandedButtonClass()
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
useEffect(() => {
const checkCustomBrand = () => {
const computedStyle = getComputedStyle(document.documentElement)
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
if (brandAccent && brandAccent !== '#6f3dfa') {
setButtonClass('branded-button-custom')
} else {
setButtonClass('branded-button-gradient')
}
}
checkCustomBrand()
window.addEventListener('resize', checkCustomBrand)
const observer = new MutationObserver(checkCustomBrand)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style', 'class'],
})
return () => {
window.removeEventListener('resize', checkCustomBrand)
observer.disconnect()
}
}, [])
return (
<>

View File

@@ -4,6 +4,7 @@ import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { X } from 'lucide-react'
import { Textarea } from '@/components/emcn'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
@@ -17,7 +18,6 @@ import { isHosted } from '@/lib/core/config/feature-flags'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import Footer from '@/app/(landing)/components/footer/footer'
import Nav from '@/app/(landing)/components/nav/nav'
@@ -493,17 +493,18 @@ export default function CareersPage() {
{/* Submit Button */}
<div className='flex justify-end pt-2'>
<BrandedButton
<Button
type='submit'
disabled={isSubmitting || submitStatus === 'success'}
loading={isSubmitting}
loadingText='Submitting'
showArrow={false}
fullWidth={false}
className='min-w-[200px]'
className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50'
size='lg'
>
{submitStatus === 'success' ? 'Submitted' : 'Submit Application'}
</BrandedButton>
{isSubmitting
? 'Submitting...'
: submitStatus === 'success'
? 'Submitted'
: 'Submit Application'}
</Button>
</div>
</form>
</section>

View File

@@ -59,7 +59,7 @@ export default function StatusIndicator() {
href={statusUrl}
target='_blank'
rel='noopener noreferrer'
className={`flex min-w-[165px] items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
className={`flex items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
aria-label={`System status: ${message}`}
>
<StatusDotIcon status={status} className='h-[6px] w-[6px]' aria-hidden='true' />

View File

@@ -11,7 +11,6 @@ import { useBrandConfig } from '@/lib/branding/branding'
import { isHosted } from '@/lib/core/config/feature-flags'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
const logger = createLogger('nav')
@@ -21,12 +20,11 @@ interface NavProps {
}
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
const [githubStars, setGithubStars] = useState('25.8k')
const [githubStars, setGithubStars] = useState('25.1k')
const [isHovered, setIsHovered] = useState(false)
const [isLoginHovered, setIsLoginHovered] = useState(false)
const router = useRouter()
const brand = useBrandConfig()
const buttonClass = useBrandedButtonClass()
useEffect(() => {
if (variant !== 'landing') return
@@ -185,7 +183,7 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
href='/signup'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`${buttonClass} group inline-flex items-center justify-center gap-2 rounded-[10px] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white transition-all`}
className='group inline-flex items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[14px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all sm:text-[16px]'
aria-label='Get started with Sim - Sign up for free'
prefetch={true}
>

View File

@@ -1,27 +0,0 @@
'use client'
import { useState } from 'react'
import { ArrowLeft, ChevronLeft } from 'lucide-react'
import Link from 'next/link'
export function BackLink() {
const [isHovered, setIsHovered] = useState(false)
return (
<Link
href='/studio'
className='group flex items-center gap-1 text-gray-600 text-sm hover:text-gray-900'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<span className='group-hover:-translate-x-0.5 inline-flex transition-transform duration-200'>
{isHovered ? (
<ArrowLeft className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
)}
</span>
Back to Sim Studio
</Link>
)
}

View File

@@ -5,10 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
import { FAQ } from '@/lib/blog/faq'
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BackLink } from '@/app/(landing)/studio/[slug]/back-link'
import { ShareButton } from '@/app/(landing)/studio/[slug]/share-button'
export async function generateStaticParams() {
const posts = await getAllPostMeta()
@@ -51,7 +48,9 @@ export default async function Page({ params }: { params: Promise<{ slug: string
/>
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
<div className='mb-6'>
<BackLink />
<Link href='/studio' className='text-gray-600 text-sm hover:text-gray-900'>
Back to Sim Studio
</Link>
</div>
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
<div className='w-full flex-shrink-0 md:w-[450px]'>
@@ -76,31 +75,28 @@ export default async function Page({ params }: { params: Promise<{ slug: string
>
{post.title}
</h1>
<div className='mt-4 flex items-center justify-between'>
<div className='flex items-center gap-3'>
{(post.authors || [post.author]).map((a, idx) => (
<div key={idx} className='flex items-center gap-2'>
{a?.avatarUrl ? (
<Avatar className='size-6'>
<AvatarImage src={a.avatarUrl} alt={a.name} />
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
</Avatar>
) : null}
<Link
href={a?.url || '#'}
target='_blank'
rel='noopener noreferrer author'
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
itemProp='author'
itemScope
itemType='https://schema.org/Person'
>
<span itemProp='name'>{a?.name}</span>
</Link>
</div>
))}
</div>
<ShareButton url={`${getBaseUrl()}/studio/${slug}`} title={post.title} />
<div className='mt-4 flex items-center gap-3'>
{(post.authors || [post.author]).map((a, idx) => (
<div key={idx} className='flex items-center gap-2'>
{a?.avatarUrl ? (
<Avatar className='size-6'>
<AvatarImage src={a.avatarUrl} alt={a.name} />
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
</Avatar>
) : null}
<Link
href={a?.url || '#'}
target='_blank'
rel='noopener noreferrer author'
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
itemProp='author'
itemScope
itemType='https://schema.org/Person'
>
<span itemProp='name'>{a?.name}</span>
</Link>
</div>
))}
</div>
</div>
</div>

View File

@@ -1,65 +0,0 @@
'use client'
import { useState } from 'react'
import { Share2 } from 'lucide-react'
import { Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
interface ShareButtonProps {
url: string
title: string
}
export function ShareButton({ url, title }: ShareButtonProps) {
const [open, setOpen] = useState(false)
const [copied, setCopied] = useState(false)
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(url)
setCopied(true)
setTimeout(() => {
setCopied(false)
setOpen(false)
}, 1000)
} catch {
setOpen(false)
}
}
const handleShareTwitter = () => {
const tweetUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`
window.open(tweetUrl, '_blank', 'noopener,noreferrer')
setOpen(false)
}
const handleShareLinkedIn = () => {
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
setOpen(false)
}
return (
<Popover
open={open}
onOpenChange={setOpen}
variant='secondary'
size='sm'
colorScheme='inverted'
>
<PopoverTrigger asChild>
<button
className='flex items-center gap-1.5 text-gray-600 text-sm hover:text-gray-900'
aria-label='Share this post'
>
<Share2 className='h-4 w-4' />
<span>Share</span>
</button>
</PopoverTrigger>
<PopoverContent align='end' minWidth={140}>
<PopoverItem onClick={handleCopyLink}>{copied ? 'Copied!' : 'Copy link'}</PopoverItem>
<PopoverItem onClick={handleShareTwitter}>Share on X</PopoverItem>
<PopoverItem onClick={handleShareLinkedIn}>Share on LinkedIn</PopoverItem>
</PopoverContent>
</Popover>
)
}

View File

@@ -22,7 +22,7 @@ export default async function StudioIndex({
? filtered.sort((a, b) => {
if (a.featured && !b.featured) return -1
if (!a.featured && b.featured) return 1
return new Date(b.date).getTime() - new Date(a.date).getTime()
return 0
})
: filtered

View File

@@ -8,7 +8,6 @@ import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getRedisClient } from '@/lib/core/config/redis'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('A2AAgentCardAPI')
@@ -96,11 +95,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<Ro
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
if (!workspaceAccess.canWrite) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const body = await request.json()
if (
@@ -166,11 +160,6 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
if (!workspaceAccess.canWrite) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
logger.info(`Deleted A2A agent: ${agentId}`)
@@ -205,11 +194,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
if (!workspaceAccess.canWrite) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const body = await request.json()
const action = body.action as 'publish' | 'unpublish' | 'refresh'

View File

@@ -16,7 +16,6 @@ import {
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getBrandConfig } from '@/lib/branding/branding'
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
import { validateExternalUrl } from '@/lib/core/security/input-validation'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { markExecutionCancelled } from '@/lib/execution/cancellation'
@@ -1119,13 +1118,17 @@ async function handlePushNotificationSet(
)
}
const urlValidation = validateExternalUrl(
params.pushNotificationConfig.url,
'Push notification URL'
)
if (!urlValidation.isValid) {
try {
const url = new URL(params.pushNotificationConfig.url)
if (url.protocol !== 'https:') {
return NextResponse.json(
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Push notification URL must use HTTPS'),
{ status: 400 }
)
}
} catch {
return NextResponse.json(
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, urlValidation.error || 'Invalid URL'),
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification URL'),
{ status: 400 }
)
}

View File

@@ -4,11 +4,6 @@ import { createLogger } from '@sim/logger'
import { and, desc, eq, inArray } from 'drizzle-orm'
import { getSession } from '@/lib/auth'
import { refreshOAuthToken } from '@/lib/oauth'
import {
getMicrosoftRefreshTokenExpiry,
isMicrosoftProvider,
PROACTIVE_REFRESH_THRESHOLD_DAYS,
} from '@/lib/oauth/microsoft'
const logger = createLogger('OAuthUtilsAPI')
@@ -210,32 +205,15 @@ export async function refreshAccessTokenIfNeeded(
}
// Decide if we should refresh: token missing OR expired
const accessTokenExpiresAt = credential.accessTokenExpiresAt
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
const expiresAt = credential.accessTokenExpiresAt
const now = new Date()
// Check if access token needs refresh (missing or expired)
const accessTokenNeedsRefresh =
!!credential.refreshToken &&
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
// Check if we should proactively refresh to prevent refresh token expiry
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
const proactiveRefreshThreshold = new Date(
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
)
const refreshTokenNeedsProactiveRefresh =
!!credential.refreshToken &&
isMicrosoftProvider(credential.providerId) &&
refreshTokenExpiresAt &&
refreshTokenExpiresAt <= proactiveRefreshThreshold
const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
const shouldRefresh =
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
const accessToken = credential.accessToken
if (shouldRefresh) {
logger.info(`[${requestId}] Refreshing token for credential`)
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
try {
const refreshedToken = await refreshOAuthToken(
credential.providerId,
@@ -249,15 +227,11 @@ export async function refreshAccessTokenIfNeeded(
userId: credential.userId,
hasRefreshToken: !!credential.refreshToken,
})
if (!accessTokenNeedsRefresh && accessToken) {
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
return accessToken
}
return null
}
// Prepare update data
const updateData: Record<string, unknown> = {
const updateData: any = {
accessToken: refreshedToken.accessToken,
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000),
updatedAt: new Date(),
@@ -269,10 +243,6 @@ export async function refreshAccessTokenIfNeeded(
updateData.refreshToken = refreshedToken.refreshToken
}
if (isMicrosoftProvider(credential.providerId)) {
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
}
// Update the token in the database
await db.update(account).set(updateData).where(eq(account.id, credentialId))
@@ -286,10 +256,6 @@ export async function refreshAccessTokenIfNeeded(
credentialId,
userId: credential.userId,
})
if (!accessTokenNeedsRefresh && accessToken) {
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
return accessToken
}
return null
}
} else if (!accessToken) {
@@ -311,27 +277,10 @@ export async function refreshTokenIfNeeded(
credentialId: string
): Promise<{ accessToken: string; refreshed: boolean }> {
// Decide if we should refresh: token missing OR expired
const accessTokenExpiresAt = credential.accessTokenExpiresAt
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
const expiresAt = credential.accessTokenExpiresAt
const now = new Date()
// Check if access token needs refresh (missing or expired)
const accessTokenNeedsRefresh =
!!credential.refreshToken &&
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
// Check if we should proactively refresh to prevent refresh token expiry
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
const proactiveRefreshThreshold = new Date(
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
)
const refreshTokenNeedsProactiveRefresh =
!!credential.refreshToken &&
isMicrosoftProvider(credential.providerId) &&
refreshTokenExpiresAt &&
refreshTokenExpiresAt <= proactiveRefreshThreshold
const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
const shouldRefresh =
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
// If token appears valid and present, return it directly
if (!shouldRefresh) {
@@ -344,17 +293,13 @@ export async function refreshTokenIfNeeded(
if (!refreshResult) {
logger.error(`[${requestId}] Failed to refresh token for credential`)
if (!accessTokenNeedsRefresh && credential.accessToken) {
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
return { accessToken: credential.accessToken, refreshed: false }
}
throw new Error('Failed to refresh token')
}
const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult
// Prepare update data
const updateData: Record<string, unknown> = {
const updateData: any = {
accessToken: refreshedToken,
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
updatedAt: new Date(),
@@ -366,10 +311,6 @@ export async function refreshTokenIfNeeded(
updateData.refreshToken = newRefreshToken
}
if (isMicrosoftProvider(credential.providerId)) {
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
}
await db.update(account).set(updateData).where(eq(account.id, credentialId))
logger.info(`[${requestId}] Successfully refreshed access token`)
@@ -390,11 +331,6 @@ export async function refreshTokenIfNeeded(
}
}
if (!accessTokenNeedsRefresh && credential.accessToken) {
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
return { accessToken: credential.accessToken, refreshed: false }
}
logger.error(`[${requestId}] Refresh failed and no valid token found in DB`, error)
throw error
}

View File

@@ -15,8 +15,7 @@ const resetPasswordSchema = z.object({
.max(100, 'Password must not exceed 100 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
.regex(/[0-9]/, 'Password must contain at least one number'),
})
export async function POST(request: NextRequest) {

View File

@@ -104,11 +104,17 @@ export async function POST(req: NextRequest) {
})
// Build execution params starting with LLM-provided arguments
// Resolve all {{ENV_VAR}} references in the arguments (deep for nested objects)
// Resolve all {{ENV_VAR}} references in the arguments
const executionParams: Record<string, any> = resolveEnvVarReferences(
toolArgs,
decryptedEnvVars,
{ deep: true }
{
resolveExactMatch: true,
allowEmbedded: true,
trimKeys: true,
onMissing: 'keep',
deep: true,
}
) as Record<string, any>
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {

View File

@@ -84,14 +84,6 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
vi.mock('@sim/logger', () => loggerMock)
vi.mock('@/lib/auth/hybrid', () => ({
checkInternalAuth: vi.fn().mockResolvedValue({
success: true,
userId: 'user-123',
authType: 'internal_jwt',
}),
}))
vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn(),
}))
@@ -118,24 +110,6 @@ describe('Function Execute API Route', () => {
})
describe('Security Tests', () => {
it('should reject unauthorized requests', async () => {
const { checkInternalAuth } = await import('@/lib/auth/hybrid')
vi.mocked(checkInternalAuth).mockResolvedValueOnce({
success: false,
error: 'Unauthorized',
})
const req = createMockRequest('POST', {
code: 'return "test"',
})
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'Unauthorized')
})
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
const req = createMockRequest('POST', {
code: 'return "test"',
@@ -302,11 +276,8 @@ describe('Function Execute API Route', () => {
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
const req = createMockRequest('POST', {
code: 'return <email>',
blockData: {
'block-123': { id: '123', subject: 'Test Email' },
},
blockNameMapping: {
email: 'block-123',
params: {
email: { id: '123', subject: 'Test Email' },
},
})
@@ -334,13 +305,9 @@ describe('Function Execute API Route', () => {
it.concurrent('should only match valid variable names in angle brackets', async () => {
const req = createMockRequest('POST', {
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
blockData: {
'block-1': 'hello',
'block-2': 'world',
},
blockNameMapping: {
validvar: 'block-1',
another_valid: 'block-2',
params: {
validVar: 'hello',
another_valid: 'world',
},
})
@@ -354,22 +321,28 @@ describe('Function Execute API Route', () => {
it.concurrent(
'should handle Gmail webhook data with email addresses containing angle brackets',
async () => {
const emailData = {
id: '123',
from: 'Waleed Latif <waleed@sim.ai>',
to: 'User <user@example.com>',
subject: 'Test Email',
bodyText: 'Hello world',
const gmailData = {
email: {
id: '123',
from: 'Waleed Latif <waleed@sim.ai>',
to: 'User <user@example.com>',
subject: 'Test Email',
bodyText: 'Hello world',
},
rawEmail: {
id: '123',
payload: {
headers: [
{ name: 'From', value: 'Waleed Latif <waleed@sim.ai>' },
{ name: 'To', value: 'User <user@example.com>' },
],
},
},
}
const req = createMockRequest('POST', {
code: 'return <email>',
blockData: {
'block-email': emailData,
},
blockNameMapping: {
email: 'block-email',
},
params: gmailData,
})
const response = await POST(req)
@@ -383,20 +356,17 @@ describe('Function Execute API Route', () => {
it.concurrent(
'should properly serialize complex email objects with special characters',
async () => {
const emailData = {
from: 'Test User <test@example.com>',
bodyHtml: '<div>HTML content with "quotes" and \'apostrophes\'</div>',
bodyText: 'Text with\nnewlines\tand\ttabs',
const complexEmailData = {
email: {
from: 'Test User <test@example.com>',
bodyHtml: '<div>HTML content with "quotes" and \'apostrophes\'</div>',
bodyText: 'Text with\nnewlines\tand\ttabs',
},
}
const req = createMockRequest('POST', {
code: 'return <email>',
blockData: {
'block-email': emailData,
},
blockNameMapping: {
email: 'block-email',
},
params: complexEmailData,
})
const response = await POST(req)
@@ -549,23 +519,18 @@ describe('Function Execute API Route', () => {
})
it.concurrent('should handle JSON serialization edge cases', async () => {
const complexData = {
special: 'chars"with\'quotes',
unicode: '🎉 Unicode content',
nested: {
deep: {
value: 'test',
},
},
}
const req = createMockRequest('POST', {
code: 'return <complexData>',
blockData: {
'block-complex': complexData,
},
blockNameMapping: {
complexdata: 'block-complex',
params: {
complexData: {
special: 'chars"with\'quotes',
unicode: '🎉 Unicode content',
nested: {
deep: {
value: 'test',
},
},
},
},
})

View File

@@ -1,16 +1,15 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
import { generateRequestId } from '@/lib/core/utils/request'
import { executeInE2B } from '@/lib/execution/e2b'
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
import {
createEnvVarPattern,
createWorkflowVariablePattern,
resolveEnvVarReferences,
} from '@/executor/utils/reference-validation'
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
@@ -19,8 +18,8 @@ export const MAX_DURATION = 210
const logger = createLogger('FunctionExecuteAPI')
const E2B_JS_WRAPPER_LINES = 3
const E2B_PYTHON_WRAPPER_LINES = 1
const E2B_JS_WRAPPER_LINES = 3 // Lines before user code: ';(async () => {', ' try {', ' const __sim_result = await (async () => {'
const E2B_PYTHON_WRAPPER_LINES = 1 // Lines before user code: 'def __sim_main__():'
type TypeScriptModule = typeof import('typescript')
@@ -135,21 +134,33 @@ function extractEnhancedError(
if (error.stack) {
enhanced.stack = error.stack
// Parse stack trace to extract line and column information
// Handle both compilation errors and runtime errors
const stackLines: string[] = error.stack.split('\n')
for (const line of stackLines) {
// Pattern 1: Compilation errors - "user-function.js:6"
let match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
// Pattern 2: Runtime errors - "at user-function.js:5:12"
if (!match) {
match = line.match(/at\s+user-function\.js:(\d+):(\d+)/)
}
// Pattern 3: Generic patterns for any line containing our filename
if (!match) {
match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
}
if (match) {
const stackLine = Number.parseInt(match[1], 10)
const stackColumn = match[2] ? Number.parseInt(match[2], 10) : undefined
// Adjust line number to account for wrapper code
// The user code starts at a specific line in our wrapper
const adjustedLine = stackLine - userCodeStartLine + 1
// Check if this is a syntax error in wrapper code caused by incomplete user code
const isWrapperSyntaxError =
stackLine > userCodeStartLine &&
error.name === 'SyntaxError' &&
@@ -157,6 +168,7 @@ function extractEnhancedError(
error.message.includes('Unexpected end of input'))
if (isWrapperSyntaxError && userCode) {
// Map wrapper syntax errors to the last line of user code
const codeLines = userCode.split('\n')
const lastUserLine = codeLines.length
enhanced.line = lastUserLine
@@ -169,6 +181,7 @@ function extractEnhancedError(
enhanced.line = adjustedLine
enhanced.column = stackColumn
// Extract the actual line content from user code
if (userCode) {
const codeLines = userCode.split('\n')
if (adjustedLine <= codeLines.length) {
@@ -179,6 +192,7 @@ function extractEnhancedError(
}
if (stackLine <= userCodeStartLine) {
// Error is in wrapper code itself
enhanced.line = stackLine
enhanced.column = stackColumn
break
@@ -186,6 +200,7 @@ function extractEnhancedError(
}
}
// Clean up stack trace to show user-relevant information
const cleanedStackLines: string[] = stackLines
.filter(
(line: string) =>
@@ -199,6 +214,9 @@ function extractEnhancedError(
}
}
// Keep original message without adding error type prefix
// The error type will be added later in createUserFriendlyErrorMessage
return enhanced
}
@@ -213,6 +231,7 @@ function formatE2BError(
userCode: string,
prologueLineCount: number
): { formattedError: string; cleanedOutput: string } {
// Calculate line offset based on language and prologue
const wrapperLines =
language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES
const totalOffset = prologueLineCount + wrapperLines
@@ -222,20 +241,27 @@ function formatE2BError(
let cleanErrorMsg = ''
if (language === CodeLanguage.Python) {
// Python error format: "Cell In[X], line Y" followed by error details
// Extract line number from the Cell reference
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
if (cellMatch) {
const originalLine = Number.parseInt(cellMatch[1], 10)
userLine = originalLine - totalOffset
}
// Extract clean error message from the error string
// Remove file references like "(detected at line X) (file.py, line Y)"
cleanErrorMsg = errorMessage
.replace(/\s*\(detected at line \d+\)/g, '')
.replace(/\s*\([^)]+\.py, line \d+\)/g, '')
.trim()
} else if (language === CodeLanguage.JavaScript) {
// JavaScript error format from E2B: "SyntaxError: /path/file.ts: Message. (line:col)\n\n 9 | ..."
// First, extract the error type and message from the first line
const firstLineEnd = errorMessage.indexOf('\n')
const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage
// Parse: "SyntaxError: /home/user/index.ts: Missing semicolon. (11:9)"
const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/)
if (jsErrorMatch) {
cleanErrorType = jsErrorMatch[1]
@@ -243,11 +269,13 @@ function formatE2BError(
const originalLine = Number.parseInt(jsErrorMatch[3], 10)
userLine = originalLine - totalOffset
} else {
// Fallback: look for line number in the arrow pointer line (> 11 |)
const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m)
if (arrowMatch) {
const originalLine = Number.parseInt(arrowMatch[1], 10)
userLine = originalLine - totalOffset
}
// Try to extract error type and message
const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/)
if (errorMatch) {
cleanErrorType = errorMatch[1]
@@ -261,11 +289,13 @@ function formatE2BError(
}
}
// Build the final clean error message
const finalErrorMsg =
cleanErrorType && cleanErrorMsg
? `${cleanErrorType}: ${cleanErrorMsg}`
: cleanErrorMsg || errorMessage
// Format with line number if available
let formattedError = finalErrorMsg
if (userLine && userLine > 0) {
const codeLines = userCode.split('\n')
@@ -281,6 +311,7 @@ function formatE2BError(
}
}
// For stdout, just return the clean error message without the full traceback
const cleanedOutput = finalErrorMsg
return { formattedError, cleanedOutput }
@@ -296,6 +327,7 @@ function createUserFriendlyErrorMessage(
): string {
let errorMessage = enhanced.message
// Add line information if available
if (enhanced.line !== undefined) {
let lineInfo = `Line ${enhanced.line}`
@@ -306,14 +338,18 @@ function createUserFriendlyErrorMessage(
errorMessage = `${lineInfo} - ${errorMessage}`
} else {
// If no line number, try to extract it from stack trace for display
if (enhanced.stack) {
const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/)
if (stackMatch) {
const line = Number.parseInt(stackMatch[1], 10)
let lineInfo = `Line ${line}`
// Try to get line content if we have userCode
if (userCode) {
const codeLines = userCode.split('\n')
// Note: stackMatch gives us VM line number, need to adjust
// This is a fallback case, so we might not have perfect line mapping
if (line <= codeLines.length) {
const lineContent = codeLines[line - 1]?.trim()
if (lineContent) {
@@ -327,6 +363,7 @@ function createUserFriendlyErrorMessage(
}
}
// Add error type prefix with consistent naming
if (enhanced.name !== 'Error') {
const errorTypePrefix =
enhanced.name === 'SyntaxError'
@@ -337,6 +374,7 @@ function createUserFriendlyErrorMessage(
? 'Reference Error'
: enhanced.name
// Only add prefix if not already present
if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) {
errorMessage = `${errorTypePrefix}: ${errorMessage}`
}
@@ -345,6 +383,9 @@ function createUserFriendlyErrorMessage(
return errorMessage
}
/**
* Resolves workflow variables with <variable.name> syntax
*/
function resolveWorkflowVariables(
code: string,
workflowVariables: Record<string, any>,
@@ -364,35 +405,39 @@ function resolveWorkflowVariables(
while ((match = regex.exec(code)) !== null) {
const variableName = match[1].trim()
// Find the variable by name (workflowVariables is indexed by ID, values are variable objects)
const foundVariable = Object.entries(workflowVariables).find(
([_, variable]) => normalizeName(variable.name || '') === variableName
)
if (!foundVariable) {
const availableVars = Object.values(workflowVariables)
.map((v) => v.name)
.filter(Boolean)
throw new Error(
`Variable "${variableName}" doesn't exist.` +
(availableVars.length > 0 ? ` Available: ${availableVars.join(', ')}` : '')
)
}
let variableValue: unknown = ''
if (foundVariable) {
const variable = foundVariable[1]
variableValue = variable.value
const variable = foundVariable[1]
let variableValue: unknown = variable.value
if (variable.value !== undefined && variable.value !== null) {
const type = variable.type === 'string' ? 'plain' : variable.type
if (type === 'number') {
variableValue = Number(variableValue)
} else if (type === 'boolean') {
variableValue = variableValue === 'true' || variableValue === true
} else if (type === 'json' && typeof variableValue === 'string') {
if (variable.value !== undefined && variable.value !== null) {
try {
variableValue = JSON.parse(variableValue)
// Handle 'string' type the same as 'plain' for backward compatibility
const type = variable.type === 'string' ? 'plain' : variable.type
// For plain text, use exactly what's entered without modifications
if (type === 'plain' && typeof variableValue === 'string') {
// Use as-is for plain text
} else if (type === 'number') {
variableValue = Number(variableValue)
} else if (type === 'boolean') {
variableValue = variableValue === 'true' || variableValue === true
} else if (type === 'json') {
try {
variableValue =
typeof variableValue === 'string' ? JSON.parse(variableValue) : variableValue
} catch {
// Keep original value if JSON parsing fails
}
}
} catch {
// Keep as-is
// Fallback to original value on error
variableValue = variable.value
}
}
}
@@ -405,9 +450,11 @@ function resolveWorkflowVariables(
})
}
// Process replacements in reverse order to maintain correct indices
for (let i = replacements.length - 1; i >= 0; i--) {
const { match: matchStr, index, variableName, variableValue } = replacements[i]
// Use variable reference approach
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
contextVariables[safeVarName] = variableValue
resolvedCode =
@@ -417,6 +464,9 @@ function resolveWorkflowVariables(
return resolvedCode
}
/**
* Resolves environment variables with {{var_name}} syntax
*/
function resolveEnvironmentVariables(
code: string,
params: Record<string, any>,
@@ -432,28 +482,32 @@ function resolveEnvironmentVariables(
const resolverVars: Record<string, string> = {}
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (value) {
resolverVars[key] = String(value)
}
})
Object.entries(envVars).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (value) {
resolverVars[key] = value
}
})
while ((match = regex.exec(code)) !== null) {
const varName = match[1].trim()
if (!(varName in resolverVars)) {
continue
}
const resolved = resolveEnvVarReferences(match[0], resolverVars, {
allowEmbedded: true,
resolveExactMatch: true,
trimKeys: true,
onMissing: 'empty',
deep: false,
})
const varValue =
typeof resolved === 'string' ? resolved : resolved == null ? '' : String(resolved)
replacements.push({
match: match[0],
index: match.index,
varName,
varValue: resolverVars[varName],
varValue: String(varValue),
})
}
@@ -469,59 +523,64 @@ function resolveEnvironmentVariables(
return resolvedCode
}
/**
* Resolves tags with <tag_name> syntax (including nested paths like <block.response.data>)
*/
function resolveTagVariables(
code: string,
blockData: Record<string, unknown>,
params: Record<string, any>,
blockData: Record<string, any>,
blockNameMapping: Record<string, string>,
blockOutputSchemas: Record<string, OutputSchema>,
contextVariables: Record<string, unknown>,
language = 'javascript'
contextVariables: Record<string, any>
): string {
let resolvedCode = code
const undefinedLiteral = language === 'python' ? 'None' : 'undefined'
const tagPattern = new RegExp(
`${REFERENCE.START}([a-zA-Z_](?:[a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])?)${REFERENCE.END}`,
`${REFERENCE.START}([a-zA-Z_][a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])${REFERENCE.END}`,
'g'
)
const tagMatches = resolvedCode.match(tagPattern) || []
for (const match of tagMatches) {
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
const blockName = pathParts[0]
const fieldPath = pathParts.slice(1)
const result = resolveBlockReference(blockName, fieldPath, {
blockNameMapping,
blockData,
blockOutputSchemas,
})
// Handle nested paths like "getrecord.response.data" or "function1.response.result"
// First try params, then blockData directly, then try with block name mapping
let tagValue = getNestedValue(params, tagName) || getNestedValue(blockData, tagName) || ''
if (!result) {
continue
}
// If not found and the path starts with a block name, try mapping the block name to ID
if (!tagValue && tagName.includes(REFERENCE.PATH_DELIMITER)) {
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
const normalizedBlockName = pathParts[0] // This should already be normalized like "function1"
let tagValue = result.value
// Direct lookup using normalized block name
const blockId = blockNameMapping[normalizedBlockName] ?? null
if (tagValue === undefined) {
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral)
continue
}
if (typeof tagValue === 'string') {
const trimmed = tagValue.trimStart()
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
tagValue = JSON.parse(tagValue)
} catch {
// Keep as string if not valid JSON
}
if (blockId) {
const remainingPath = pathParts.slice(1).join('.')
const fullPath = `${blockId}.${remainingPath}`
tagValue = getNestedValue(blockData, fullPath) || ''
}
}
const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}`
// If the value is a stringified JSON, parse it back to object
if (
typeof tagValue === 'string' &&
tagValue.length > 100 &&
(tagValue.startsWith('{') || tagValue.startsWith('['))
) {
try {
tagValue = JSON.parse(tagValue)
} catch (e) {
// Keep as string if parsing fails
}
}
// Instead of injecting large JSON directly, create a variable reference
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
contextVariables[safeVarName] = tagValue
// Replace the template with a variable reference
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}
@@ -537,31 +596,44 @@ function resolveTagVariables(
*/
function resolveCodeVariables(
code: string,
params: Record<string, unknown>,
params: Record<string, any>,
envVars: Record<string, string> = {},
blockData: Record<string, unknown> = {},
blockData: Record<string, any> = {},
blockNameMapping: Record<string, string> = {},
blockOutputSchemas: Record<string, OutputSchema> = {},
workflowVariables: Record<string, unknown> = {},
language = 'javascript'
): { resolvedCode: string; contextVariables: Record<string, unknown> } {
workflowVariables: Record<string, any> = {}
): { resolvedCode: string; contextVariables: Record<string, any> } {
let resolvedCode = code
const contextVariables: Record<string, unknown> = {}
const contextVariables: Record<string, any> = {}
// Resolve workflow variables with <variable.name> syntax first
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
// Resolve environment variables with {{var_name}} syntax
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
resolvedCode = resolveTagVariables(
resolvedCode,
params,
blockData,
blockNameMapping,
blockOutputSchemas,
contextVariables,
language
contextVariables
)
return { resolvedCode, contextVariables }
}
/**
* Get nested value from object using dot notation path
*/
function getNestedValue(obj: any, path: string): any {
if (!obj || !path) return undefined
return path.split('.').reduce((current, key) => {
return current && typeof current === 'object' ? current[key] : undefined
}, obj)
}
/**
* Remove one trailing newline from stdout
* This handles the common case where print() or console.log() adds a trailing \n
@@ -582,12 +654,6 @@ export async function POST(req: NextRequest) {
let resolvedCode = '' // Store resolved code for error reporting
try {
const auth = await checkInternalAuth(req)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized function execution attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await req.json()
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
@@ -600,12 +666,12 @@ export async function POST(req: NextRequest) {
envVars = {},
blockData = {},
blockNameMapping = {},
blockOutputSchemas = {},
workflowVariables = {},
workflowId,
isCustomTool = false,
} = body
// Extract internal parameters that shouldn't be passed to the execution context
const executionParams = { ...params }
executionParams._context = undefined
@@ -617,21 +683,21 @@ export async function POST(req: NextRequest) {
isCustomTool,
})
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
// Resolve variables in the code with workflow environment variables
const codeResolution = resolveCodeVariables(
code,
executionParams,
envVars,
blockData,
blockNameMapping,
blockOutputSchemas,
workflowVariables,
lang
workflowVariables
)
resolvedCode = codeResolution.resolvedCode
const contextVariables = codeResolution.contextVariables
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
// Extract imports once for JavaScript code (reuse later to avoid double extraction)
let jsImports = ''
let jsRemainingCode = resolvedCode
let hasImports = false
@@ -641,22 +707,31 @@ export async function POST(req: NextRequest) {
jsImports = extractionResult.imports
jsRemainingCode = extractionResult.remainingCode
// Check for ES6 imports or CommonJS require statements
// ES6 imports are extracted by the TypeScript parser
// Also check for require() calls which indicate external dependencies
const hasRequireStatements = /require\s*\(\s*['"`]/.test(resolvedCode)
hasImports = jsImports.trim().length > 0 || hasRequireStatements
}
// Python always requires E2B
if (lang === CodeLanguage.Python && !isE2bEnabled) {
throw new Error(
'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.'
)
}
// JavaScript with imports requires E2B
if (lang === CodeLanguage.JavaScript && hasImports && !isE2bEnabled) {
throw new Error(
'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.'
)
}
// Use E2B if:
// - E2B is enabled AND
// - Not a custom tool AND
// - (Python OR JavaScript with imports)
const useE2B =
isE2bEnabled &&
!isCustomTool &&
@@ -669,10 +744,13 @@ export async function POST(req: NextRequest) {
language: lang,
})
let prologue = ''
const epilogue = ''
if (lang === CodeLanguage.JavaScript) {
// Track prologue lines for error adjustment
let prologueLineCount = 0
// Reuse the imports we already extracted earlier
const imports = jsImports
const remainingCode = jsRemainingCode
@@ -687,11 +765,7 @@ export async function POST(req: NextRequest) {
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
if (v === undefined) {
prologue += `const ${k} = undefined;\n`
} else {
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
}
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
prologueLineCount++
}
@@ -708,7 +782,7 @@ export async function POST(req: NextRequest) {
' }',
'})();',
].join('\n')
const codeForE2B = importSection + prologue + wrapped
const codeForE2B = importSection + prologue + wrapped + epilogue
const execStart = Date.now()
const {
@@ -730,6 +804,7 @@ export async function POST(req: NextRequest) {
error: e2bError,
})
// If there was an execution error, format it properly
if (e2bError) {
const { formattedError, cleanedOutput } = formatE2BError(
e2bError,
@@ -753,7 +828,7 @@ export async function POST(req: NextRequest) {
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
})
}
// Track prologue lines for error adjustment
let prologueLineCount = 0
prologue += 'import json\n'
prologueLineCount++
@@ -762,11 +837,7 @@ export async function POST(req: NextRequest) {
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
if (v === undefined) {
prologue += `${k} = None\n`
} else {
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
}
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
prologueLineCount++
}
const wrapped = [
@@ -775,7 +846,7 @@ export async function POST(req: NextRequest) {
'__sim_result__ = __sim_main__()',
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
].join('\n')
const codeForE2B = prologue + wrapped
const codeForE2B = prologue + wrapped + epilogue
const execStart = Date.now()
const {
@@ -797,6 +868,7 @@ export async function POST(req: NextRequest) {
error: e2bError,
})
// If there was an execution error, format it properly
if (e2bError) {
const { formattedError, cleanedOutput } = formatE2BError(
e2bError,
@@ -825,6 +897,7 @@ export async function POST(req: NextRequest) {
const wrapperLines = ['(async () => {', ' try {']
if (isCustomTool) {
wrapperLines.push(' // For custom tools, make parameters directly accessible')
Object.keys(executionParams).forEach((key) => {
wrapperLines.push(` const ${key} = params.${key};`)
})
@@ -858,10 +931,12 @@ export async function POST(req: NextRequest) {
})
const ivmError = isolatedResult.error
// Adjust line number for prepended param destructuring in custom tools
let adjustedLine = ivmError.line
let adjustedLineContent = ivmError.lineContent
if (prependedLineCount > 0 && ivmError.line !== undefined) {
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
// Get line content from original user code, not the prepended code
const codeLines = resolvedCode.split('\n')
if (adjustedLine <= codeLines.length) {
adjustedLineContent = codeLines[adjustedLine - 1]?.trim()

View File

@@ -157,7 +157,7 @@ describe('Knowledge Base Documents API Route', () => {
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
'kb-123',
{
enabledFilter: undefined,
includeDisabled: false,
search: undefined,
limit: 50,
offset: 0,
@@ -166,7 +166,7 @@ describe('Knowledge Base Documents API Route', () => {
)
})
it('should return documents with default filter', async () => {
it('should filter disabled documents by default', async () => {
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
const { getDocuments } = await import('@/lib/knowledge/documents/service')
@@ -194,7 +194,7 @@ describe('Knowledge Base Documents API Route', () => {
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
'kb-123',
{
enabledFilter: undefined,
includeDisabled: false,
search: undefined,
limit: 50,
offset: 0,
@@ -203,7 +203,7 @@ describe('Knowledge Base Documents API Route', () => {
)
})
it('should filter documents by enabled status when requested', async () => {
it('should include disabled documents when requested', async () => {
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
const { getDocuments } = await import('@/lib/knowledge/documents/service')
@@ -223,7 +223,7 @@ describe('Knowledge Base Documents API Route', () => {
},
})
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?enabledFilter=disabled'
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?includeDisabled=true'
const req = new Request(url, { method: 'GET' }) as any
const { GET } = await import('@/app/api/knowledge/[id]/documents/route')
@@ -233,7 +233,7 @@ describe('Knowledge Base Documents API Route', () => {
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
'kb-123',
{
enabledFilter: 'disabled',
includeDisabled: true,
search: undefined,
limit: 50,
offset: 0,
@@ -361,7 +361,8 @@ describe('Knowledge Base Documents API Route', () => {
expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith(
validDocumentData,
'kb-123',
expect.any(String)
expect.any(String),
'user-123'
)
})
@@ -469,7 +470,8 @@ describe('Knowledge Base Documents API Route', () => {
expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith(
validBulkData.documents,
'kb-123',
expect.any(String)
expect.any(String),
'user-123'
)
expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled()
})

View File

@@ -5,7 +5,6 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import {
bulkDocumentOperation,
bulkDocumentOperationByFilter,
createDocumentRecords,
createSingleDocument,
getDocuments,
@@ -58,20 +57,13 @@ const BulkCreateDocumentsSchema = z.object({
bulk: z.literal(true),
})
const BulkUpdateDocumentsSchema = z
.object({
operation: z.enum(['enable', 'disable', 'delete']),
documentIds: z
.array(z.string())
.min(1, 'At least one document ID is required')
.max(100, 'Cannot operate on more than 100 documents at once')
.optional(),
selectAll: z.boolean().optional(),
enabledFilter: z.enum(['all', 'enabled', 'disabled']).optional(),
})
.refine((data) => data.selectAll || (data.documentIds && data.documentIds.length > 0), {
message: 'Either selectAll must be true or documentIds must be provided',
})
const BulkUpdateDocumentsSchema = z.object({
operation: z.enum(['enable', 'disable', 'delete']),
documentIds: z
.array(z.string())
.min(1, 'At least one document ID is required')
.max(100, 'Cannot operate on more than 100 documents at once'),
})
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
@@ -98,17 +90,14 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
}
const url = new URL(req.url)
const enabledFilter = url.searchParams.get('enabledFilter') as
| 'all'
| 'enabled'
| 'disabled'
| null
const includeDisabled = url.searchParams.get('includeDisabled') === 'true'
const search = url.searchParams.get('search') || undefined
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
const offset = Number.parseInt(url.searchParams.get('offset') || '0')
const sortByParam = url.searchParams.get('sortBy')
const sortOrderParam = url.searchParams.get('sortOrder')
// Validate sort parameters
const validSortFields: DocumentSortField[] = [
'filename',
'fileSize',
@@ -116,7 +105,6 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
'chunkCount',
'uploadedAt',
'processingStatus',
'enabled',
]
const validSortOrders: SortOrder[] = ['asc', 'desc']
@@ -132,7 +120,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
const result = await getDocuments(
knowledgeBaseId,
{
enabledFilter: enabledFilter || undefined,
includeDisabled,
search,
limit,
offset,
@@ -202,7 +190,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const createdDocuments = await createDocumentRecords(
validatedData.documents,
knowledgeBaseId,
requestId
requestId,
userId
)
logger.info(
@@ -261,10 +250,16 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
throw validationError
}
} else {
// Handle single document creation
try {
const validatedData = CreateDocumentSchema.parse(body)
const newDocument = await createSingleDocument(validatedData, knowledgeBaseId, requestId)
const newDocument = await createSingleDocument(
validatedData,
knowledgeBaseId,
requestId,
userId
)
try {
const { PlatformEvents } = await import('@/lib/core/telemetry')
@@ -299,6 +294,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
} catch (error) {
logger.error(`[${requestId}] Error creating document`, error)
// Check if it's a storage limit error
const errorMessage = error instanceof Error ? error.message : 'Failed to create document'
const isStorageLimitError =
errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit')
@@ -335,22 +331,16 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
try {
const validatedData = BulkUpdateDocumentsSchema.parse(body)
const { operation, documentIds, selectAll, enabledFilter } = validatedData
const { operation, documentIds } = validatedData
try {
let result
if (selectAll) {
result = await bulkDocumentOperationByFilter(
knowledgeBaseId,
operation,
enabledFilter,
requestId
)
} else if (documentIds && documentIds.length > 0) {
result = await bulkDocumentOperation(knowledgeBaseId, operation, documentIds, requestId)
} else {
return NextResponse.json({ error: 'No documents specified' }, { status: 400 })
}
const result = await bulkDocumentOperation(
knowledgeBaseId,
operation,
documentIds,
requestId,
session.user.id
)
return NextResponse.json({
success: true,

View File

@@ -1,10 +1,11 @@
import { createLogger } from '@sim/logger'
import type { NextRequest } from 'next/server'
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
import { McpClient } from '@/lib/mcp/client'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config'
import type { McpTransport } from '@/lib/mcp/types'
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
const logger = createLogger('McpServerTestAPI')
@@ -18,6 +19,30 @@ function isUrlBasedTransport(transport: McpTransport): boolean {
return transport === 'streamable-http'
}
/**
* Resolve environment variables in strings
*/
function resolveEnvVars(value: string, envVars: Record<string, string>): string {
const missingVars: string[] = []
const resolvedValue = resolveEnvVarReferences(value, envVars, {
allowEmbedded: true,
resolveExactMatch: true,
trimKeys: true,
onMissing: 'keep',
deep: false,
missingKeys: missingVars,
}) as string
if (missingVars.length > 0) {
const uniqueMissing = Array.from(new Set(missingVars))
uniqueMissing.forEach((envKey) => {
logger.warn(`Environment variable "${envKey}" not found in MCP server test`)
})
}
return resolvedValue
}
interface TestConnectionRequest {
name: string
transport: McpTransport
@@ -71,30 +96,39 @@ export const POST = withMcpAuth('write')(
)
}
// Build initial config for resolution
const initialConfig = {
let resolvedUrl = body.url
let resolvedHeaders = body.headers || {}
try {
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
if (resolvedUrl) {
resolvedUrl = resolveEnvVars(resolvedUrl, envVars)
}
const resolvedHeadersObj: Record<string, string> = {}
for (const [key, value] of Object.entries(resolvedHeaders)) {
resolvedHeadersObj[key] = resolveEnvVars(value, envVars)
}
resolvedHeaders = resolvedHeadersObj
} catch (envError) {
logger.warn(
`[${requestId}] Failed to resolve environment variables, using raw values:`,
envError
)
}
const testConfig: McpServerConfig = {
id: `test-${requestId}`,
name: body.name,
transport: body.transport,
url: body.url,
headers: body.headers || {},
url: resolvedUrl,
headers: resolvedHeaders,
timeout: body.timeout || 10000,
retries: 1, // Only one retry for tests
enabled: true,
}
// Resolve env vars using shared utility (non-strict mode for testing)
const { config: testConfig, missingVars } = await resolveMcpConfigEnvVars(
initialConfig,
userId,
workspaceId,
{ strict: false }
)
if (missingVars.length > 0) {
logger.warn(`[${requestId}] Some environment variables not found:`, { missingVars })
}
const testSecurityPolicy = {
requireConsent: false,
auditLevel: 'none' as const,

View File

@@ -3,9 +3,7 @@ import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import type { StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'
@@ -22,11 +20,6 @@ export async function POST(request: NextRequest) {
const startTime = Date.now()
try {
const auth = await checkInternalAuth(request, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
logger.info(`[${requestId}] Provider API request started`, {
timestamp: new Date().toISOString(),
userAgent: request.headers.get('User-Agent'),
@@ -92,13 +85,6 @@ export async function POST(request: NextRequest) {
verbosity,
})
if (workspaceId) {
const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId)
if (!workspaceAccess.hasAccess) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}
let finalApiKey: string | undefined = apiKey
try {
if (provider === 'vertex' && vertexCredential) {

View File

@@ -3,7 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createA2AClient } from '@/lib/a2a/utils'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateExternalUrl } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -40,18 +39,6 @@ export async function POST(request: NextRequest) {
const body = await request.json()
const validatedData = A2ASetPushNotificationSchema.parse(body)
const urlValidation = validateExternalUrl(validatedData.webhookUrl, 'Webhook URL')
if (!urlValidation.isValid) {
logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error })
return NextResponse.json(
{
success: false,
error: urlValidation.error,
},
{ status: 400 }
)
}
logger.info(`[${requestId}] A2A set push notification request`, {
agentUrl: validatedData.agentUrl,
taskId: validatedData.taskId,

View File

@@ -181,7 +181,7 @@ describe('Custom Tools API Routes', () => {
}))
vi.doMock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
checkHybridAuth: vi.fn().mockResolvedValue({
success: true,
userId: 'user-123',
authType: 'session',
@@ -254,7 +254,7 @@ describe('Custom Tools API Routes', () => {
)
vi.doMock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
checkHybridAuth: vi.fn().mockResolvedValue({
success: false,
error: 'Unauthorized',
}),
@@ -304,7 +304,7 @@ describe('Custom Tools API Routes', () => {
describe('POST /api/tools/custom', () => {
it('should reject unauthorized requests', async () => {
vi.doMock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
checkHybridAuth: vi.fn().mockResolvedValue({
success: false,
error: 'Unauthorized',
}),
@@ -390,7 +390,7 @@ describe('Custom Tools API Routes', () => {
it('should prevent unauthorized deletion of user-scoped tool', async () => {
vi.doMock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
checkHybridAuth: vi.fn().mockResolvedValue({
success: true,
userId: 'user-456',
authType: 'session',
@@ -413,7 +413,7 @@ describe('Custom Tools API Routes', () => {
it('should reject unauthorized requests', async () => {
vi.doMock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
checkHybridAuth: vi.fn().mockResolvedValue({
success: false,
error: 'Unauthorized',
}),

View File

@@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, desc, eq, isNull, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
@@ -42,8 +42,8 @@ export async function GET(request: NextRequest) {
const workflowId = searchParams.get('workflowId')
try {
// Use session/internal auth to support session and internal JWT (no API key access)
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
// Use hybrid auth to support session, API key, and internal JWT
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized custom tools access attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
@@ -69,8 +69,8 @@ export async function GET(request: NextRequest) {
}
// Check workspace permissions
// For internal JWT with workflowId: checkSessionOrInternalAuth already resolved userId from workflow owner
// For session: verify user has access to the workspace
// For internal JWT with workflowId: checkHybridAuth already resolved userId from workflow owner
// For session/API key: verify user has access to the workspace
// For legacy (no workspaceId): skip workspace check, rely on userId match
if (resolvedWorkspaceId && !(authResult.authType === 'internal_jwt' && workflowId)) {
const userPermission = await getUserEntityPermissions(
@@ -116,8 +116,8 @@ export async function POST(req: NextRequest) {
const requestId = generateRequestId()
try {
// Use session/internal auth (no API key access)
const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
// Use hybrid auth (though this endpoint is only called from UI)
const authResult = await checkHybridAuth(req, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized custom tools update attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
@@ -193,8 +193,8 @@ export async function DELETE(request: NextRequest) {
}
try {
// Use session/internal auth (no API key access)
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
// Use hybrid auth (though this endpoint is only called from UI)
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized custom tool deletion attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateNumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail add label attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail archive attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail delete attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail mark read attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail mark unread attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail move attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail remove label attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail unarchive attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`)

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateImageUrl } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
const imageUrl = url.searchParams.get('url')
const requestId = generateRequestId()
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.error(`[${requestId}] Authentication failed for image proxy:`, authResult.error)
return new NextResponse('Unauthorized', { status: 401 })

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { Resend } from 'resend'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized mail send attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Teams chat delete attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { StorageService } from '@/lib/uploads'
@@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Mistral parse attempt`, {

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLDeleteAPI')
@@ -22,12 +21,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL delete attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = DeleteSchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLExecuteAPI')
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL execute attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = ExecuteSchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLInsertAPI')
@@ -43,12 +42,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL insert attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = InsertSchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLIntrospectAPI')
@@ -20,12 +19,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL introspect attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = IntrospectSchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLQueryAPI')
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL query attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = QuerySchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLUpdateAPI')
@@ -41,12 +40,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL update attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = UpdateSchema.parse(body)

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import * as XLSX from 'xlsx'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import {
@@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Outlook copy attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Outlook delete attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Outlook mark read attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Outlook mark unread attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Outlook move attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createPostgresConnection, executeDelete } from '@/app/api/tools/postgresql/utils'
const logger = createLogger('PostgreSQLDeleteAPI')
@@ -22,12 +21,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL delete attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = DeleteSchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
createPostgresConnection,
executeQuery,
@@ -25,12 +24,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL execute attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = ExecuteSchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createPostgresConnection, executeInsert } from '@/app/api/tools/postgresql/utils'
const logger = createLogger('PostgreSQLInsertAPI')
@@ -43,12 +42,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL insert attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = InsertSchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createPostgresConnection, executeIntrospect } from '@/app/api/tools/postgresql/utils'
const logger = createLogger('PostgreSQLIntrospectAPI')
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL introspect attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = IntrospectSchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createPostgresConnection, executeQuery } from '@/app/api/tools/postgresql/utils'
const logger = createLogger('PostgreSQLQueryAPI')
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL query attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = QuerySchema.parse(body)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createPostgresConnection, executeUpdate } from '@/app/api/tools/postgresql/utils'
const logger = createLogger('PostgreSQLUpdateAPI')
@@ -41,12 +40,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL update attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = UpdateSchema.parse(body)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { StorageService } from '@/lib/uploads'
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Pulse parse attempt`, {

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { StorageService } from '@/lib/uploads'
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Reducto parse attempt`, {

View File

@@ -2,7 +2,7 @@ import { CopyObjectCommand, type ObjectCannedACL, S3Client } from '@aws-sdk/clie
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 copy object attempt: ${authResult.error}`)

View File

@@ -2,7 +2,7 @@ import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 delete object attempt: ${authResult.error}`)

View File

@@ -2,7 +2,7 @@ import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 list objects attempt: ${authResult.error}`)

View File

@@ -2,7 +2,7 @@ import { type ObjectCannedACL, PutObjectCommand, S3Client } from '@aws-sdk/clien
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { SEARCH_TOOL_COST } from '@/lib/billing/constants'
import { env } from '@/lib/core/config/env'
import { executeTool } from '@/tools'
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
const { searchParams: urlParams } = new URL(request.url)
const workflowId = urlParams.get('workflowId') || undefined
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
const errorMessage = workflowId ? 'Workflow not found' : authResult.error || 'Unauthorized'

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import type { SFTPWrapper } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import {
createSftpConnection,
@@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SFTP delete attempt: ${authResult.error}`)

View File

@@ -2,7 +2,7 @@ import path from 'path'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils'
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SFTP download attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import {
createSftpConnection,
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SFTP list attempt: ${authResult.error}`)

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import type { SFTPWrapper } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import {
createSftpConnection,
@@ -60,7 +60,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SFTP mkdir attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -44,7 +44,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SFTP upload attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`)

View File

@@ -1,6 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
export const dynamic = 'force-dynamic'
@@ -13,7 +13,7 @@ const SlackAddReactionSchema = z.object({
export async function POST(request: NextRequest) {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json(

View File

@@ -1,6 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
export const dynamic = 'force-dynamic'
@@ -12,7 +12,7 @@ const SlackDeleteMessageSchema = z.object({
export async function POST(request: NextRequest) {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json(

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { openDMChannel } from '../utils'
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Slack read messages attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { sendSlackMessage } from '../utils'
@@ -26,7 +26,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Slack send attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Slack update message attempt: ${authResult.error}`)

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { env } from '@/lib/core/config/env'
import { generateRequestId } from '@/lib/core/utils/request'
import { type SMSOptions, sendSMS } from '@/lib/messaging/sms/service'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SMS send attempt: ${authResult.error}`)

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import nodemailer from 'nodemailer'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createSSHConnection, escapeShellArg, executeSSHCommand } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHCheckCommandExistsAPI')
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH check command exists attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = CheckCommandExistsSchema.parse(body)

View File

@@ -3,7 +3,6 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import type { Client, SFTPWrapper, Stats } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
createSSHConnection,
getFileType,
@@ -40,15 +39,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH check file exists attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = CheckFileExistsSchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
createSSHConnection,
escapeShellArg,
@@ -28,15 +27,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH create directory attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = CreateDirectorySchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },
@@ -59,6 +53,7 @@ export async function POST(request: NextRequest) {
const dirPath = sanitizePath(params.path)
const escapedPath = escapeShellArg(dirPath)
// Check if directory already exists
const checkResult = await executeSSHCommand(
client,
`test -d '${escapedPath}' && echo "exists"`
@@ -75,6 +70,7 @@ export async function POST(request: NextRequest) {
})
}
// Create directory
const mkdirFlag = params.recursive ? '-p' : ''
const command = `mkdir ${mkdirFlag} -m ${params.permissions} '${escapedPath}'`
const result = await executeSSHCommand(client, command)

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
createSSHConnection,
escapeShellArg,
@@ -28,15 +27,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH delete file attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = DeleteFileSchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },
@@ -59,6 +53,7 @@ export async function POST(request: NextRequest) {
const filePath = sanitizePath(params.path)
const escapedPath = escapeShellArg(filePath)
// Check if path exists
const checkResult = await executeSSHCommand(
client,
`test -e '${escapedPath}' && echo "exists"`
@@ -67,6 +62,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: `Path does not exist: ${filePath}` }, { status: 404 })
}
// Build delete command
let command: string
if (params.recursive) {
command = params.force ? `rm -rf '${escapedPath}'` : `rm -r '${escapedPath}'`

View File

@@ -4,7 +4,6 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import type { Client, SFTPWrapper } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHDownloadFileAPI')
@@ -35,15 +34,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH download file attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = DownloadFileSchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createSSHConnection, executeSSHCommand, sanitizeCommand } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHExecuteCommandAPI')
@@ -22,15 +21,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH execute command attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = ExecuteCommandSchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },
@@ -50,6 +44,7 @@ export async function POST(request: NextRequest) {
})
try {
// Build command with optional working directory
let command = sanitizeCommand(params.command)
if (params.workingDirectory) {
command = `cd "${params.workingDirectory}" && ${command}`

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createSSHConnection, escapeShellArg, executeSSHCommand } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHExecuteScriptAPI')
@@ -23,15 +22,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH execute script attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = ExecuteScriptSchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },
@@ -51,10 +45,13 @@ export async function POST(request: NextRequest) {
})
try {
// Create a temporary script file, execute it, and clean up
const scriptPath = `/tmp/sim_script_${requestId}.sh`
const escapedScriptPath = escapeShellArg(scriptPath)
const escapedInterpreter = escapeShellArg(params.interpreter)
// Build the command to create, execute, and clean up the script
// Note: heredoc with quoted delimiter ('SIMEOF') prevents variable expansion
let command = `cat > '${escapedScriptPath}' << 'SIMEOF'
${params.script}
SIMEOF

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createSSHConnection, executeSSHCommand } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHGetSystemInfoAPI')
@@ -20,15 +19,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH get system info attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = GetSystemInfoSchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },

View File

@@ -3,7 +3,6 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import type { Client, FileEntry, SFTPWrapper } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
createSSHConnection,
getFileType,
@@ -61,15 +60,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH list directory attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = ListDirectorySchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
createSSHConnection,
escapeShellArg,
@@ -28,16 +27,9 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH move/rename attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = MoveRenameSchema.parse(body)
// Validate SSH authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },

View File

@@ -3,7 +3,6 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import type { Client, SFTPWrapper } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHReadFileContentAPI')
@@ -36,12 +35,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH read file content attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = ReadFileContentSchema.parse(body)

View File

@@ -3,7 +3,6 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import type { Client, SFTPWrapper } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHUploadFileAPI')
@@ -38,12 +37,6 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH upload file attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = UploadFileSchema.parse(body)

View File

@@ -3,7 +3,6 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import type { Client, SFTPWrapper } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHWriteFileContentAPI')
@@ -37,15 +36,10 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized SSH write file content attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const params = WriteFileContentSchema.parse(body)
// Validate authentication
if (!params.password && !params.privateKey) {
return NextResponse.json(
{ error: 'Either password or privateKey must be provided' },

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { extractAudioFromVideo, isVideoFile } from '@/lib/audio/extractor'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import type { UserFile } from '@/executor/types'
import type { TranscriptSegment } from '@/tools/stt/types'
@@ -40,7 +40,7 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] STT transcription request started`)
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

Some files were not shown because too many files have changed in this diff Show More