Compare commits

..

46 Commits

Author SHA1 Message Date
Waleed Latif
243e01bb21 extracted duplicate code into helper for resolving user from jwt 2026-01-24 01:43:06 -08:00
Waleed Latif
28b51b7791 move session check higher in checkSessionOrInternalAuth 2026-01-24 01:39:01 -08:00
Waleed Latif
b1bcd9a796 updated agent handler 2026-01-24 01:22:02 -08:00
Waleed Latif
a45c019c7c test(security): update function execute tests for checkInternalAuth 2026-01-23 19:57:28 -08:00
Waleed Latif
f78659e56e fix(security): restrict API key access on internal-only routes 2026-01-23 19:44:21 -08:00
Waleed
428781ce7d feat(blog): enterprise post (#2961)
* feat(blog): enterprise post

* added more images, styling

* more content

* updated v0-5 post

* remove unused transition

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-01-23 18:58:00 -08:00
Waleed
f0ee67f3ed improvement(helm): add internal ingress support and same-host path consolidation (#2960)
* improvement(helm): add internal ingress support and same-host path consolidation

* improvement(helm): clean up ingress template comments

Simplify verbose inline Helm comments and section dividers to match the
minimal style used in services.yaml.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(helm): add missing copilot path consolidation for realtime host

When copilot.host equals realtime.host but differs from app.host,
copilot paths were not being routed. Added logic to consolidate
copilot paths into the realtime rule for this scenario.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* improvement(helm): follow ingress best practices

- Remove orphan comments that appeared when services were disabled
- Add documentation about path ordering requirements
- Paths rendered in order: realtime, copilot, app (specific before catch-all)
- Clean template output matching industry Helm chart standards

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 18:44:18 -08:00
Waleed
f44594c380 fix(security): add authentication and input validation to API routes (#2959)
* fix(security): add authentication and input validation to API routes

* moved utils

* remove extraneous commetns

* removed unused dep
2026-01-23 17:48:39 -08:00
Emir Karabeg
6464cfa7f2 fix(logs): refresh logic to refresh logs details (#2958) 2026-01-23 17:22:33 -08:00
Waleed
7f4edc85ef fix(billing): handle missing userStats and prevent crashes (#2956)
* fix(billing): handle missing userStats and prevent crashes

* fix(billing): correct import path for getFilledPillColor

* fix(billing): add Number.isFinite check to lastPeriodCost
2026-01-23 14:45:11 -08:00
Siddharth Ganesan
efef91ece0 improvement(copilot): fast mode, subagent tool responses and allow preferences (#2955)
* Improvements

* Fix actions mapping

* Remove console logs
2026-01-23 13:03:05 -08:00
Waleed
64efeaa2e6 feat(admin): add credits endpoint to issue credits to users (#2954)
* feat(admin): add credits endpoint to issue credits to users

* fix(admin): use existing credit functions and handle enterprise seats

* fix(admin): reject NaN and Infinity in amount validation

* styling

* fix(admin): validate userId and email are strings
2026-01-23 11:33:13 -08:00
Waleed
9b72b52b33 feat(blog): v0.5 release post (#2953)
* feat(blog): v0.5 post

* improvement(blog): simplify title and remove code block header

- Simplified blog title from "Introducing Sim Studio v0.5" to "Introducing Sim v0.5"
- Removed language label header and copy button from code blocks for cleaner appearance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ack PR comments

* small styling improvements

* created system to create post-specific components

* updated componnet

* cache invalidation

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 09:07:53 -08:00
Emir Karabeg
1467862488 improvement(logs): trace span, details (#2952)
* improvement(action-bar): ordering

* improvement(logs): details, trace span
2026-01-22 19:50:20 -08:00
Waleed
7f2262857c improvement(kb): add document filtering, select all, and React Query migration (#2951)
* improvement(kb): add document filtering, select all, and React Query migration

* test(kb): update tests for enabledFilter and removed userId params

* fix(kb): remove non-null assertion, add explicit guard
2026-01-22 19:25:16 -08:00
Vikhyath Mondreti
1b309b50e6 fix(idempotency): add conflict target to atomicallyClaimDb query + remove redundant db namespace tracking (#2950)
* fix(idempotency): add conflict target to atomicallyClaimDb query

* delete needs to account for namespace

* simplify namespace filtering logic

* fix cleanup

* consistent target
2026-01-22 18:38:08 -08:00
Waleed
f765b83a26 chore(deps): bump posthog-js to 1.334.1 (#2948) 2026-01-22 18:06:05 -08:00
Vikhyath Mondreti
aa99db6fdd fix(subflows): tag dropdown + resolution logic (#2949)
* fix(subflows): tag dropdown + resolution logic

* fixes;

* revert parallel change
2026-01-22 17:57:55 -08:00
Waleed
748793e07d fix(executor): handle condition dead-end branches in loops (#2944) 2026-01-22 13:30:11 -08:00
Siddharth Ganesan
91da7e183a fix(copilot): always allow, credential masking (#2947)
* Fix always allow, credential validation

* Credential masking

* Autoload
2026-01-22 13:07:16 -08:00
Waleed
ab09a5ad23 feat(router): expose reasoning output in router v2 block (#2945) 2026-01-22 12:43:57 -08:00
Vikhyath Mondreti
fcd0240db6 fix(resolver): consolidate reference resolution (#2941)
* fix(resolver): consolidate code to resolve references

* fix edge cases

* use already formatted error

* fix multi index

* fix backwards compat reachability

* handle backwards compatibility accurately

* use shared constant correctly
2026-01-22 12:38:50 -08:00
Waleed
4e4149792a fix(gmail): expose messageId field in read email block (#2943) 2026-01-22 11:46:34 -08:00
Waleed
9a8b591257 improvement(helm): add per-deployment extraVolumes support (#2942) 2026-01-22 11:35:23 -08:00
Waleed
f3ae3f8442 fix(executor): stop parallel execution when block errors (#2940) 2026-01-22 11:34:40 -08:00
Waleed
66dfe2c6b2 improvement(workflow-item): stabilize avatar layout and fix name truncation (#2939)
* improvement(workflow-item): stabilize avatar layout and fix name truncation

* fix(avatars): revert overflow bg to hardcoded color for contrast
2026-01-22 11:26:47 -08:00
Waleed
376f7cb571 fix(auth): add genericOAuth providers to trustedProviders (#2937) 2026-01-21 22:44:30 -08:00
Vikhyath Mondreti
42159c23b9 improvement(copilot): tool configs to show nested props (#2936) 2026-01-21 20:02:59 -08:00
Emir Karabeg
2f0f246002 feat(workflow-block): preview (#2935) 2026-01-21 19:12:28 -08:00
Waleed
900d3ef9ea fix(workflow-selector): use dedicated selector for workflow dropdown (#2934) 2026-01-21 18:38:03 -08:00
Waleed
f3fcc28f89 fix(auth): handle EMAIL_NOT_VERIFIED in onError callback (#2932)
* fix(auth): handle EMAIL_NOT_VERIFIED in onError callback

* refactor(auth): extract redirectToVerify helper to reduce duplication
2026-01-21 18:34:49 -08:00
Vikhyath Mondreti
7cfdf46724 fix(token-refresh): microsoft, notion, x, linear (#2933)
* fix(microsoft): proactive refresh needed

* fix(x): missing token refresh flag

* notion and linear missing flag too

* address bugbot comment
2026-01-21 18:30:53 -08:00
Vikhyath Mondreti
d681451297 fix(null-bodies): empty bodies handling (#2931)
* fix(null-statuses): empty bodies handling

* address bugbot comment
2026-01-21 18:10:33 -08:00
Waleed
5987a6d060 fix(custom-tools): remove unsafe title fallback in getCustomTool (#2929)
* fix(custom-tools): remove unsafe title fallback in getCustomTool

* fix(custom-tools): restore title fallback in getCustomTool lookup

Custom tools are referenced by title (custom_${title}), not database ID.
The title fallback is required for client-side tool resolution to work.
2026-01-21 17:36:10 -08:00
Waleed
e2ccefb2f4 improvement(ui): use BrandedButton and BrandedLink components (#2930)
- Refactor auth forms to use BrandedButton component
- Add BrandedLink component for changelog page
- Reduce code duplication in login, signup, reset-password forms
- Update star count default value
2026-01-21 17:25:30 -08:00
Waleed
103b31a569 fix(stores): remove dead code causing log spam on startup (#2927)
* fix(stores): remove dead code causing log spam on startup

* fix(stores): replace custom tools zustand store with react query cache
2026-01-21 16:08:26 -08:00
Waleed
004e058353 fix(messages-input): fix cursor alignment and auto-resize with overlay (#2926)
* fix(messages-input): fix cursor alignment and auto-resize with overlay

* fixed remaining zustand warnings
2026-01-21 15:30:13 -08:00
Vikhyath Mondreti
5157f0bbb2 fix(resolver): agent response format, input formats, root level (#2925)
* fix(resolvers): agent response format, input formats, root level

* fix response block initial seeding

* fix tests
2026-01-21 14:55:23 -08:00
Waleed
8bbcf31b83 fix(action-bar): duplicate subflows with children (#2923)
* fix(action-bar): duplicate subflows with children

* fix(action-bar): add validateTriggerPaste for subflow duplicate
2026-01-21 14:54:29 -08:00
Waleed
9e814315dd fix(auth): improve reset password flow and consolidate brand detection (#2924)
* fix(auth): improve reset password flow and consolidate brand detection

* fix(auth): set errorHandled for EMAIL_NOT_VERIFIED to prevent duplicate error

* fix(auth): clear success message on login errors

* chore(auth): fix import order per lint
2026-01-21 14:42:14 -08:00
Waleed
0ea0256623 chore(helm): add env vars for Vertex AI, orgs, and telemetry (#2922) 2026-01-21 11:36:16 -08:00
Waleed
fb8868c854 fix(notifications): text overflow with line-clamp (#2921) 2026-01-21 10:20:21 -08:00
Waleed
ea4964052d fix(logger): use direct env access for webpack inlining (#2920) 2026-01-21 10:14:40 -08:00
Waleed
268e2f114f fix(zustand): updated to useShallow from deprecated createWithEqualityFn (#2919) 2026-01-21 09:47:48 -08:00
Vikhyath Mondreti
5988d0e46f fix(ring): duplicate should clear original block (#2916)
* fix(ring): duplicate should clear original block

* rename correctly
2026-01-21 02:40:58 -08:00
Vikhyath Mondreti
145db9d8c3 fix(http): options not parsed accurately (#2914)
* fix(http): options not parsed accurately

* fix lint

* remove boilerplate code'
2026-01-21 01:36:29 -08:00
318 changed files with 18850 additions and 4239 deletions

View File

@@ -2,10 +2,9 @@
import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
import { 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,
@@ -22,8 +21,10 @@ 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')
@@ -105,8 +106,7 @@ export default function LoginPage({
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const [isButtonHovered, setIsButtonHovered] = useState(false)
const buttonClass = useBrandedButtonClass()
const [callbackUrl, setCallbackUrl] = useState('/workspace')
const [isInviteFlow, setIsInviteFlow] = useState(false)
@@ -114,7 +114,6 @@ 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
@@ -123,6 +122,7 @@ 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,32 +139,12 @@ export default function LoginPage({
const inviteFlow = searchParams.get('invite_flow') === 'true'
setIsInviteFlow(inviteFlow)
}
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')
const resetSuccess = searchParams.get('resetSuccess') === 'true'
if (resetSuccess) {
setResetSuccessMessage('Password reset successful. Please sign in with your new password.')
}
}
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(() => {
@@ -202,6 +182,13 @@ 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()
@@ -221,6 +208,7 @@ export default function LoginPage({
try {
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
let errorHandled = false
const result = await client.signIn.email(
{
@@ -231,11 +219,16 @@ export default function LoginPage({
{
onError: (ctx) => {
logger.error('Login error:', ctx.error)
const errorMessage: string[] = ['Invalid email or password']
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('BAD_REQUEST') ||
ctx.error.message?.includes('Email and password sign in is not enabled')
@@ -271,6 +264,7 @@ export default function LoginPage({
errorMessage.push('Too many requests. Please wait a moment before trying again.')
}
setResetSuccessMessage(null)
setPasswordErrors(errorMessage)
setShowValidationError(true)
},
@@ -278,15 +272,25 @@ 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')) {
if (typeof window !== 'undefined') {
sessionStorage.setItem('verificationEmail', email)
}
router.push('/verify')
redirectToVerify(email)
return
}
@@ -400,6 +404,13 @@ 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`}>
@@ -482,24 +493,14 @@ export default function LoginPage({
</div>
</div>
<Button
<BrandedButton
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'
>
<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>
Sign in
</BrandedButton>
</form>
)}
@@ -610,25 +611,15 @@ export default function LoginPage({
<p>{resetStatus.message}</p>
</div>
)}
<Button
<BrandedButton
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'
>
<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>
Send Reset Link
</BrandedButton>
</div>
</DialogContent>
</Dialog>

View File

@@ -1,12 +1,12 @@
'use client'
import { useEffect, useState } from 'react'
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
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,36 +27,6 @@ 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)
@@ -94,24 +64,14 @@ export function RequestResetForm({
)}
</div>
<Button
<BrandedButton
type='submit'
disabled={isSubmitting}
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'
loading={isSubmitting}
loadingText='Sending'
>
<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>
Send Reset Link
</BrandedButton>
</form>
)
}
@@ -138,35 +98,6 @@ 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()
@@ -296,24 +227,14 @@ export function SetNewPasswordForm({
)}
</div>
<Button
disabled={isSubmitting || !token}
<BrandedButton
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={isSubmitting || !token}
loading={isSubmitting}
loadingText='Resetting'
>
<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>
Reset Password
</BrandedButton>
</form>
)
}

View File

@@ -2,10 +2,9 @@
import { Suspense, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
import { 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'
@@ -14,8 +13,10 @@ 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')
@@ -95,8 +96,7 @@ function SignupFormContent({
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const [redirectUrl, setRedirectUrl] = useState('')
const [isInviteFlow, setIsInviteFlow] = useState(false)
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const [isButtonHovered, setIsButtonHovered] = useState(false)
const buttonClass = useBrandedButtonClass()
const [name, setName] = useState('')
const [nameErrors, setNameErrors] = useState<string[]>([])
@@ -126,31 +126,6 @@ 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[] => {
@@ -500,24 +475,14 @@ function SignupFormContent({
</div>
</div>
<Button
<BrandedButton
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'
>
<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>
Create account
</BrandedButton>
</form>
)}

View File

@@ -13,6 +13,7 @@ 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')
@@ -57,7 +58,7 @@ export default function SSOForm() {
const [email, setEmail] = useState('')
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const buttonClass = useBrandedButtonClass()
const [callbackUrl, setCallbackUrl] = useState('/workspace')
useEffect(() => {
@@ -90,31 +91,6 @@ 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,6 +8,7 @@ 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
@@ -58,34 +59,7 @@ function VerificationForm({
setCountdown(30)
}
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()
}
}, [])
const buttonClass = useBrandedButtonClass()
return (
<>

View File

@@ -4,7 +4,6 @@ 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 {
@@ -18,6 +17,7 @@ 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,18 +493,17 @@ export default function CareersPage() {
{/* Submit Button */}
<div className='flex justify-end pt-2'>
<Button
<BrandedButton
type='submit'
disabled={isSubmitting || submitStatus === 'success'}
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'
loading={isSubmitting}
loadingText='Submitting'
showArrow={false}
fullWidth={false}
className='min-w-[200px]'
>
{isSubmitting
? 'Submitting...'
: submitStatus === 'success'
? 'Submitted'
: 'Submit Application'}
</Button>
{submitStatus === 'success' ? 'Submitted' : 'Submit Application'}
</BrandedButton>
</div>
</form>
</section>

View File

@@ -59,7 +59,7 @@ export default function StatusIndicator() {
href={statusUrl}
target='_blank'
rel='noopener noreferrer'
className={`flex items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
className={`flex min-w-[165px] 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,6 +11,7 @@ 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')
@@ -20,11 +21,12 @@ interface NavProps {
}
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
const [githubStars, setGithubStars] = useState('25.1k')
const [githubStars, setGithubStars] = useState('25.8k')
const [isHovered, setIsHovered] = useState(false)
const [isLoginHovered, setIsLoginHovered] = useState(false)
const router = useRouter()
const brand = useBrandConfig()
const buttonClass = useBrandedButtonClass()
useEffect(() => {
if (variant !== 'landing') return
@@ -183,7 +185,7 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
href='/signup'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
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]'
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`}
aria-label='Get started with Sim - Sign up for free'
prefetch={true}
>

View File

@@ -0,0 +1,27 @@
'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,7 +5,10 @@ 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()
@@ -48,9 +51,7 @@ 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'>
<Link href='/studio' className='text-gray-600 text-sm hover:text-gray-900'>
Back to Sim Studio
</Link>
<BackLink />
</div>
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
<div className='w-full flex-shrink-0 md:w-[450px]'>
@@ -75,28 +76,31 @@ export default async function Page({ params }: { params: Promise<{ slug: string
>
{post.title}
</h1>
<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 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>
</div>
</div>

View File

@@ -0,0 +1,65 @@
'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 0
return new Date(b.date).getTime() - new Date(a.date).getTime()
})
: filtered

View File

@@ -8,6 +8,7 @@ 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')
@@ -95,6 +96,11 @@ 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 (
@@ -160,6 +166,11 @@ 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}`)
@@ -194,6 +205,11 @@ 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,6 +16,7 @@ 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'
@@ -1118,17 +1119,13 @@ async function handlePushNotificationSet(
)
}
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 {
const urlValidation = validateExternalUrl(
params.pushNotificationConfig.url,
'Push notification URL'
)
if (!urlValidation.isValid) {
return NextResponse.json(
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification URL'),
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, urlValidation.error || 'Invalid URL'),
{ status: 400 }
)
}

View File

@@ -4,6 +4,11 @@ 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')
@@ -205,15 +210,32 @@ export async function refreshAccessTokenIfNeeded(
}
// Decide if we should refresh: token missing OR expired
const expiresAt = credential.accessTokenExpiresAt
const accessTokenExpiresAt = credential.accessTokenExpiresAt
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
const now = new Date()
const shouldRefresh =
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
// 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 accessToken = credential.accessToken
if (shouldRefresh) {
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
logger.info(`[${requestId}] Refreshing token for credential`)
try {
const refreshedToken = await refreshOAuthToken(
credential.providerId,
@@ -227,11 +249,15 @@ 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: any = {
const updateData: Record<string, unknown> = {
accessToken: refreshedToken.accessToken,
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000),
updatedAt: new Date(),
@@ -243,6 +269,10 @@ 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))
@@ -256,6 +286,10 @@ 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) {
@@ -277,10 +311,27 @@ export async function refreshTokenIfNeeded(
credentialId: string
): Promise<{ accessToken: string; refreshed: boolean }> {
// Decide if we should refresh: token missing OR expired
const expiresAt = credential.accessTokenExpiresAt
const accessTokenExpiresAt = credential.accessTokenExpiresAt
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
const now = new Date()
const shouldRefresh =
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
// 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
// If token appears valid and present, return it directly
if (!shouldRefresh) {
@@ -293,13 +344,17 @@ 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: any = {
const updateData: Record<string, unknown> = {
accessToken: refreshedToken,
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
updatedAt: new Date(),
@@ -311,6 +366,10 @@ 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`)
@@ -331,6 +390,11 @@ 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,7 +15,8 @@ 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(/[0-9]/, 'Password must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
})
export async function POST(request: NextRequest) {

View File

@@ -11,7 +11,7 @@ import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { setFormAuthCookie, validateFormAuth } from '@/app/api/form/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -36,7 +36,7 @@ async function getWorkflowInputSchema(workflowId: string): Promise<any[]> {
.from(workflowBlocks)
.where(eq(workflowBlocks.workflowId, workflowId))
const startBlock = blocks.find((block) => isValidStartBlockType(block.type))
const startBlock = blocks.find((block) => isInputDefinitionTrigger(block.type))
if (!startBlock) {
return []

View File

@@ -84,6 +84,14 @@ 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(),
}))
@@ -110,6 +118,24 @@ 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"',
@@ -276,8 +302,11 @@ describe('Function Execute API Route', () => {
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
const req = createMockRequest('POST', {
code: 'return <email>',
params: {
email: { id: '123', subject: 'Test Email' },
blockData: {
'block-123': { id: '123', subject: 'Test Email' },
},
blockNameMapping: {
email: 'block-123',
},
})
@@ -305,9 +334,13 @@ 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>',
params: {
validVar: 'hello',
another_valid: 'world',
blockData: {
'block-1': 'hello',
'block-2': 'world',
},
blockNameMapping: {
validvar: 'block-1',
another_valid: 'block-2',
},
})
@@ -321,28 +354,22 @@ describe('Function Execute API Route', () => {
it.concurrent(
'should handle Gmail webhook data with email addresses containing angle brackets',
async () => {
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 emailData = {
id: '123',
from: 'Waleed Latif <waleed@sim.ai>',
to: 'User <user@example.com>',
subject: 'Test Email',
bodyText: 'Hello world',
}
const req = createMockRequest('POST', {
code: 'return <email>',
params: gmailData,
blockData: {
'block-email': emailData,
},
blockNameMapping: {
email: 'block-email',
},
})
const response = await POST(req)
@@ -356,17 +383,20 @@ describe('Function Execute API Route', () => {
it.concurrent(
'should properly serialize complex email objects with special characters',
async () => {
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 emailData = {
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>',
params: complexEmailData,
blockData: {
'block-email': emailData,
},
blockNameMapping: {
email: 'block-email',
},
})
const response = await POST(req)
@@ -519,18 +549,23 @@ 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>',
params: {
complexData: {
special: 'chars"with\'quotes',
unicode: '🎉 Unicode content',
nested: {
deep: {
value: 'test',
},
},
},
blockData: {
'block-complex': complexData,
},
blockNameMapping: {
complexdata: 'block-complex',
},
})

View File

@@ -1,15 +1,16 @@
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'
@@ -18,8 +19,8 @@ export const MAX_DURATION = 210
const logger = createLogger('FunctionExecuteAPI')
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__():'
const E2B_JS_WRAPPER_LINES = 3
const E2B_PYTHON_WRAPPER_LINES = 1
type TypeScriptModule = typeof import('typescript')
@@ -134,33 +135,21 @@ 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' &&
@@ -168,7 +157,6 @@ 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
@@ -181,7 +169,6 @@ 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) {
@@ -192,7 +179,6 @@ function extractEnhancedError(
}
if (stackLine <= userCodeStartLine) {
// Error is in wrapper code itself
enhanced.line = stackLine
enhanced.column = stackColumn
break
@@ -200,7 +186,6 @@ function extractEnhancedError(
}
}
// Clean up stack trace to show user-relevant information
const cleanedStackLines: string[] = stackLines
.filter(
(line: string) =>
@@ -214,9 +199,6 @@ function extractEnhancedError(
}
}
// Keep original message without adding error type prefix
// The error type will be added later in createUserFriendlyErrorMessage
return enhanced
}
@@ -231,7 +213,6 @@ 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
@@ -241,27 +222,20 @@ 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]
@@ -269,13 +243,11 @@ 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]
@@ -289,13 +261,11 @@ 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')
@@ -311,7 +281,6 @@ function formatE2BError(
}
}
// For stdout, just return the clean error message without the full traceback
const cleanedOutput = finalErrorMsg
return { formattedError, cleanedOutput }
@@ -327,7 +296,6 @@ function createUserFriendlyErrorMessage(
): string {
let errorMessage = enhanced.message
// Add line information if available
if (enhanced.line !== undefined) {
let lineInfo = `Line ${enhanced.line}`
@@ -338,18 +306,14 @@ 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) {
@@ -363,7 +327,6 @@ function createUserFriendlyErrorMessage(
}
}
// Add error type prefix with consistent naming
if (enhanced.name !== 'Error') {
const errorTypePrefix =
enhanced.name === 'SyntaxError'
@@ -374,7 +337,6 @@ function createUserFriendlyErrorMessage(
? 'Reference Error'
: enhanced.name
// Only add prefix if not already present
if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) {
errorMessage = `${errorTypePrefix}: ${errorMessage}`
}
@@ -383,9 +345,6 @@ function createUserFriendlyErrorMessage(
return errorMessage
}
/**
* Resolves workflow variables with <variable.name> syntax
*/
function resolveWorkflowVariables(
code: string,
workflowVariables: Record<string, any>,
@@ -405,39 +364,35 @@ 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
)
let variableValue: unknown = ''
if (foundVariable) {
const variable = foundVariable[1]
variableValue = variable.value
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(', ')}` : '')
)
}
if (variable.value !== undefined && variable.value !== null) {
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') {
try {
// 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
}
}
variableValue = JSON.parse(variableValue)
} catch {
// Fallback to original value on error
variableValue = variable.value
// Keep as-is
}
}
}
@@ -450,11 +405,9 @@ 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 =
@@ -464,9 +417,6 @@ function resolveWorkflowVariables(
return resolvedCode
}
/**
* Resolves environment variables with {{var_name}} syntax
*/
function resolveEnvironmentVariables(
code: string,
params: Record<string, any>,
@@ -482,32 +432,28 @@ function resolveEnvironmentVariables(
const resolverVars: Record<string, string> = {}
Object.entries(params).forEach(([key, value]) => {
if (value) {
if (value !== undefined && value !== null) {
resolverVars[key] = String(value)
}
})
Object.entries(envVars).forEach(([key, value]) => {
if (value) {
if (value !== undefined && value !== null) {
resolverVars[key] = value
}
})
while ((match = regex.exec(code)) !== null) {
const varName = match[1].trim()
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)
if (!(varName in resolverVars)) {
continue
}
replacements.push({
match: match[0],
index: match.index,
varName,
varValue: String(varValue),
varValue: resolverVars[varName],
})
}
@@ -523,64 +469,59 @@ function resolveEnvironmentVariables(
return resolvedCode
}
/**
* Resolves tags with <tag_name> syntax (including nested paths like <block.response.data>)
*/
function resolveTagVariables(
code: string,
params: Record<string, any>,
blockData: Record<string, any>,
blockData: Record<string, unknown>,
blockNameMapping: Record<string, string>,
contextVariables: Record<string, any>
blockOutputSchemas: Record<string, OutputSchema>,
contextVariables: Record<string, unknown>,
language = 'javascript'
): 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)
// 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) || ''
const result = resolveBlockReference(blockName, fieldPath, {
blockNameMapping,
blockData,
blockOutputSchemas,
})
// 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"
if (!result) {
continue
}
// Direct lookup using normalized block name
const blockId = blockNameMapping[normalizedBlockName] ?? null
let tagValue = result.value
if (blockId) {
const remainingPath = pathParts.slice(1).join('.')
const fullPath = `${blockId}.${remainingPath}`
tagValue = getNestedValue(blockData, fullPath) || ''
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 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, '_')}`
const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}`
contextVariables[safeVarName] = tagValue
// Replace the template with a variable reference
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}
@@ -596,44 +537,31 @@ function resolveTagVariables(
*/
function resolveCodeVariables(
code: string,
params: Record<string, any>,
params: Record<string, unknown>,
envVars: Record<string, string> = {},
blockData: Record<string, any> = {},
blockData: Record<string, unknown> = {},
blockNameMapping: Record<string, string> = {},
workflowVariables: Record<string, any> = {}
): { resolvedCode: string; contextVariables: Record<string, any> } {
blockOutputSchemas: Record<string, OutputSchema> = {},
workflowVariables: Record<string, unknown> = {},
language = 'javascript'
): { resolvedCode: string; contextVariables: Record<string, unknown> } {
let resolvedCode = code
const contextVariables: Record<string, any> = {}
const contextVariables: Record<string, unknown> = {}
// 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,
contextVariables
blockOutputSchemas,
contextVariables,
language
)
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
@@ -654,6 +582,12 @@ 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')
@@ -666,12 +600,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
@@ -683,21 +617,21 @@ export async function POST(req: NextRequest) {
isCustomTool,
})
// Resolve variables in the code with workflow environment variables
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
const codeResolution = resolveCodeVariables(
code,
executionParams,
envVars,
blockData,
blockNameMapping,
workflowVariables
blockOutputSchemas,
workflowVariables,
lang
)
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
@@ -707,31 +641,22 @@ 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 &&
@@ -744,13 +669,10 @@ 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
@@ -765,7 +687,11 @@ 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)) {
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
if (v === undefined) {
prologue += `const ${k} = undefined;\n`
} else {
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
}
prologueLineCount++
}
@@ -782,7 +708,7 @@ export async function POST(req: NextRequest) {
' }',
'})();',
].join('\n')
const codeForE2B = importSection + prologue + wrapped + epilogue
const codeForE2B = importSection + prologue + wrapped
const execStart = Date.now()
const {
@@ -804,7 +730,6 @@ export async function POST(req: NextRequest) {
error: e2bError,
})
// If there was an execution error, format it properly
if (e2bError) {
const { formattedError, cleanedOutput } = formatE2BError(
e2bError,
@@ -828,7 +753,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++
@@ -837,7 +762,11 @@ 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)) {
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
if (v === undefined) {
prologue += `${k} = None\n`
} else {
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
}
prologueLineCount++
}
const wrapped = [
@@ -846,7 +775,7 @@ export async function POST(req: NextRequest) {
'__sim_result__ = __sim_main__()',
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
].join('\n')
const codeForE2B = prologue + wrapped + epilogue
const codeForE2B = prologue + wrapped
const execStart = Date.now()
const {
@@ -868,7 +797,6 @@ export async function POST(req: NextRequest) {
error: e2bError,
})
// If there was an execution error, format it properly
if (e2bError) {
const { formattedError, cleanedOutput } = formatE2BError(
e2bError,
@@ -897,7 +825,6 @@ 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};`)
})
@@ -931,12 +858,10 @@ 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',
{
includeDisabled: false,
enabledFilter: undefined,
search: undefined,
limit: 50,
offset: 0,
@@ -166,7 +166,7 @@ describe('Knowledge Base Documents API Route', () => {
)
})
it('should filter disabled documents by default', async () => {
it('should return documents with default filter', 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',
{
includeDisabled: false,
enabledFilter: undefined,
search: undefined,
limit: 50,
offset: 0,
@@ -203,7 +203,7 @@ describe('Knowledge Base Documents API Route', () => {
)
})
it('should include disabled documents when requested', async () => {
it('should filter documents by enabled status 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?includeDisabled=true'
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?enabledFilter=disabled'
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',
{
includeDisabled: true,
enabledFilter: 'disabled',
search: undefined,
limit: 50,
offset: 0,
@@ -361,8 +361,7 @@ describe('Knowledge Base Documents API Route', () => {
expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith(
validDocumentData,
'kb-123',
expect.any(String),
'user-123'
expect.any(String)
)
})
@@ -470,8 +469,7 @@ describe('Knowledge Base Documents API Route', () => {
expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith(
validBulkData.documents,
'kb-123',
expect.any(String),
'user-123'
expect.any(String)
)
expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled()
})

View File

@@ -5,6 +5,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import {
bulkDocumentOperation,
bulkDocumentOperationByFilter,
createDocumentRecords,
createSingleDocument,
getDocuments,
@@ -57,13 +58,20 @@ 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'),
})
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',
})
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
@@ -90,14 +98,17 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
}
const url = new URL(req.url)
const includeDisabled = url.searchParams.get('includeDisabled') === 'true'
const enabledFilter = url.searchParams.get('enabledFilter') as
| 'all'
| 'enabled'
| 'disabled'
| null
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',
@@ -105,6 +116,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
'chunkCount',
'uploadedAt',
'processingStatus',
'enabled',
]
const validSortOrders: SortOrder[] = ['asc', 'desc']
@@ -120,7 +132,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
const result = await getDocuments(
knowledgeBaseId,
{
includeDisabled,
enabledFilter: enabledFilter || undefined,
search,
limit,
offset,
@@ -190,8 +202,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const createdDocuments = await createDocumentRecords(
validatedData.documents,
knowledgeBaseId,
requestId,
userId
requestId
)
logger.info(
@@ -250,16 +261,10 @@ 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,
userId
)
const newDocument = await createSingleDocument(validatedData, knowledgeBaseId, requestId)
try {
const { PlatformEvents } = await import('@/lib/core/telemetry')
@@ -294,7 +299,6 @@ 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')
@@ -331,16 +335,22 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
try {
const validatedData = BulkUpdateDocumentsSchema.parse(body)
const { operation, documentIds } = validatedData
const { operation, documentIds, selectAll, enabledFilter } = validatedData
try {
const result = await bulkDocumentOperation(
knowledgeBaseId,
operation,
documentIds,
requestId,
session.user.id
)
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 })
}
return NextResponse.json({
success: true,

View File

@@ -3,7 +3,9 @@ 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'
@@ -20,6 +22,11 @@ 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'),
@@ -85,6 +92,13 @@ 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,6 +3,7 @@ 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'
@@ -39,6 +40,18 @@ 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', () => ({
checkHybridAuth: vi.fn().mockResolvedValue({
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
success: true,
userId: 'user-123',
authType: 'session',
@@ -254,7 +254,7 @@ describe('Custom Tools API Routes', () => {
)
vi.doMock('@/lib/auth/hybrid', () => ({
checkHybridAuth: vi.fn().mockResolvedValue({
checkSessionOrInternalAuth: 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', () => ({
checkHybridAuth: vi.fn().mockResolvedValue({
checkSessionOrInternalAuth: 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', () => ({
checkHybridAuth: vi.fn().mockResolvedValue({
checkSessionOrInternalAuth: 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', () => ({
checkHybridAuth: vi.fn().mockResolvedValue({
checkSessionOrInternalAuth: 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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkSessionOrInternalAuth } 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 hybrid auth to support session, API key, and internal JWT
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
// Use session/internal auth to support session and internal JWT (no API key access)
const authResult = await checkSessionOrInternalAuth(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: checkHybridAuth already resolved userId from workflow owner
// For session/API key: verify user has access to the workspace
// For internal JWT with workflowId: checkSessionOrInternalAuth already resolved userId from workflow owner
// For session: 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 hybrid auth (though this endpoint is only called from UI)
const authResult = await checkHybridAuth(req, { requireWorkflowId: false })
// Use session/internal auth (no API key access)
const authResult = await checkSessionOrInternalAuth(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 hybrid auth (though this endpoint is only called from UI)
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
// Use session/internal auth (no API key access)
const authResult = await checkSessionOrInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Mistral parse attempt`, {

View File

@@ -2,6 +2,7 @@ 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')
@@ -21,6 +22,12 @@ 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,6 +2,7 @@ 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')
@@ -20,6 +21,12 @@ 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,6 +2,7 @@ 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')
@@ -42,6 +43,12 @@ 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,6 +2,7 @@ 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')
@@ -19,6 +20,12 @@ 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,6 +2,7 @@ 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')
@@ -20,6 +21,12 @@ 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,6 +2,7 @@ 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')
@@ -40,6 +41,12 @@ 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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)

View File

@@ -2,6 +2,7 @@ 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')
@@ -21,6 +22,12 @@ 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,6 +2,7 @@ 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,
@@ -24,6 +25,12 @@ 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,6 +2,7 @@ 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')
@@ -42,6 +43,12 @@ 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,6 +2,7 @@ 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')
@@ -20,6 +21,12 @@ 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,6 +2,7 @@ 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')
@@ -20,6 +21,12 @@ 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,6 +2,7 @@ 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')
@@ -40,6 +41,12 @@ 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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`)

View File

@@ -2,6 +2,7 @@ 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')
@@ -20,6 +21,12 @@ 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,6 +3,7 @@ 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,
@@ -39,10 +40,15 @@ 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,6 +2,7 @@ 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,
@@ -27,10 +28,15 @@ 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' },
@@ -53,7 +59,6 @@ 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"`
@@ -70,7 +75,6 @@ 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,6 +2,7 @@ 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,
@@ -27,10 +28,15 @@ 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' },
@@ -53,7 +59,6 @@ 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"`
@@ -62,7 +67,6 @@ 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,6 +4,7 @@ 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')
@@ -34,10 +35,15 @@ 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,6 +2,7 @@ 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')
@@ -21,10 +22,15 @@ 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' },
@@ -44,7 +50,6 @@ 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,6 +2,7 @@ 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')
@@ -22,10 +23,15 @@ 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' },
@@ -45,13 +51,10 @@ 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,6 +2,7 @@ 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')
@@ -19,10 +20,15 @@ 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,6 +3,7 @@ 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,
@@ -60,10 +61,15 @@ 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,6 +2,7 @@ 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,
@@ -27,9 +28,16 @@ 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,6 +3,7 @@ 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')
@@ -35,6 +36,12 @@ 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,6 +3,7 @@ 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')
@@ -37,6 +38,12 @@ 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,6 +3,7 @@ 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')
@@ -36,10 +37,15 @@ 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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
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 { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } 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 checkHybridAuth(request, {
const authResult = await checkInternalAuth(request, {
requireWorkflowId: false,
})

View File

@@ -2,7 +2,7 @@ import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAwsRegion,
validateExternalUrl,
@@ -292,7 +292,7 @@ export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Textract parse attempt`, {

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { StorageService } from '@/lib/uploads'
@@ -10,7 +10,7 @@ const logger = createLogger('ProxyTTSAPI')
export async function POST(request: NextRequest) {
try {
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.error('Authentication failed for TTS proxy:', authResult.error)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

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