v0.6.50: ppt/doc/pdf worker isolation, docs, chat, sidebar improvements

This commit is contained in:
Vikhyath Mondreti
2026-04-17 22:11:10 -07:00
committed by GitHub
403 changed files with 4828 additions and 2156 deletions

View File

@@ -30,5 +30,25 @@ const shortId = generateShortId()
const tiny = generateShortId(8)
```
## Common Utilities
Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations:
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
```typescript
// ✗ Bad
await new Promise(resolve => setTimeout(resolve, 1000))
const msg = error instanceof Error ? error.message : String(error)
const err = error instanceof Error ? error : new Error(String(error))
// ✓ Good
import { sleep, toError } from '@/lib/core/utils/helpers'
await sleep(1000)
const msg = toError(error).message
const err = toError(error)
```
## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.

View File

@@ -37,5 +37,25 @@ const shortId = generateShortId()
const tiny = generateShortId(8)
```
## Common Utilities
Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations:
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
```typescript
// ✗ Bad
await new Promise(resolve => setTimeout(resolve, 1000))
const msg = error instanceof Error ? error.message : String(error)
const err = error instanceof Error ? error : new Error(String(error))
// ✓ Good
import { sleep, toError } from '@/lib/core/utils/helpers'
await sleep(1000)
const msg = toError(error).message
const err = toError(error)
```
## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.

View File

@@ -0,0 +1,85 @@
---
description: Isolated-vm sandbox worker security policy. Hard rules for anything that lives in the worker child process that runs user code.
globs: ["apps/sim/lib/execution/isolated-vm-worker.cjs", "apps/sim/lib/execution/isolated-vm.ts", "apps/sim/lib/execution/sandbox/**", "apps/sim/sandbox-tasks/**"]
---
# Sim Sandbox — Worker Security Policy
The isolated-vm worker child process at
`apps/sim/lib/execution/isolated-vm-worker.cjs` runs untrusted user code inside
V8 isolates. The process itself is a trust boundary. Everything in this rule is
about what must **never** live in that process.
## Hard rules
1. **No app credentials in the worker process**. The worker must not hold, load,
or receive via IPC: database URLs, Redis URLs, AWS keys, Stripe keys,
session-signing keys, encryption keys, OAuth client secrets, internal API
secrets, or any LLM / email / search provider API keys. If you catch yourself
`require`'ing `@/lib/auth`, `@sim/db`, `@/lib/uploads/core/storage-service`,
or anything that imports `env` directly inside the worker, stop and use a
host-side broker instead.
2. **Host-side brokers own all credentialed work**. The worker can only access
resources through `ivm.Reference` / `ivm.Callback` bridges back to the host
process. Today the only broker is `workspaceFileBroker`
(`apps/sim/lib/execution/sandbox/brokers/workspace-file.ts`); adding a new
one requires co-reviewing this file.
3. **Host-side brokers must scope every resource access to a single tenant**.
The `SandboxBrokerContext` always carries `workspaceId`. Any new broker that
accesses storage, DB, or an external API must use `ctx.workspaceId` to scope
the lookup — never accept a raw path, key, or URL from isolate code without
validation.
4. **Nothing that runs in the isolate is trusted, even if we wrote it**. The
task `bootstrap` and `finalize` strings in `apps/sim/sandbox-tasks/` execute
inside the isolate. They must treat `globalThis` as adversarial — no pulling
values from it that might have been mutated by user code. The hardening
script in `executeTask` undefines dangerous globals before user code runs.
## Why
A V8 JIT bug (Chrome ships these roughly monthly) gives an attacker a native
code primitive inside the process that owns whatever that process can reach.
If the worker only holds `isolated-vm` + a single narrow workspace-file broker,
a V8 escape leaks one tenant's files. If the worker holds a Stripe key or a DB
connection, a V8 escape leaks the service.
The original `doc-worker.cjs` vulnerability (CVE-class, 225 production secrets
leaked via `/proc/1/environ`) was the forcing function for this architecture.
Keep the blast radius small.
## Checklist for changes to `isolated-vm-worker.cjs`
Before landing any change that adds a new `require(...)` or `process.send(...)`
payload or `ivm.Reference` wrapper in the worker:
- [ ] Does it load a credential, key, connection string, or secret? If yes,
move it host-side and expose as a broker.
- [ ] Does it import from `@/lib/auth`, `@sim/db`, `@/lib/uploads/core/*`,
`@/lib/core/config/env`, or any module that reads `process.env` of the
main app? If yes, same — move host-side.
- [ ] Does it expose a resource that's workspace-scoped without taking a
`workspaceId`? If yes, re-scope.
- [ ] Did you update the broker limits (`IVM_MAX_BROKER_ARGS_JSON_CHARS`,
`IVM_MAX_BROKER_RESULT_JSON_CHARS`, `IVM_MAX_BROKERS_PER_EXECUTION`) if
the new broker can emit large payloads or fire frequently?
## What the worker *may* hold
- `isolated-vm` module
- Node built-ins: `node:fs` (only for reading the checked-in bundle `.cjs`
files) and `node:path`
- The three prebuilt library bundles under
`apps/sim/lib/execution/sandbox/bundles/*.cjs`
- IPC message handlers for `execute`, `cancel`, `fetchResponse`,
`brokerResponse`
The worker deliberately has **no host-side logger**. All errors and
diagnostics flow through IPC back to the host, which has `@sim/logger`. Do
not add `createLogger` or console-based logging to the worker — it would
require pulling the main app's config / env, which is exactly what this
rule is preventing.
Anything else is suspect.

View File

@@ -8,6 +8,7 @@ You are a professional software engineer. All code must follow best practices: a
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
- **Common Utilities**: Use shared helpers from `@/lib/core/utils/helpers` instead of inline implementations. `sleep(ms)` for delays, `toError(e)` to normalize caught values.
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
## Architecture

View File

@@ -1,6 +1,6 @@
'use client'
import { useState } from 'react'
import { useRef, useState } from 'react'
import { cn, getAssetUrl } from '@/lib/utils'
import { Lightbox } from './lightbox'
@@ -50,11 +50,14 @@ export function ActionImage({ src, alt, enableLightbox = true }: ActionImageProp
}
export function ActionVideo({ src, alt, enableLightbox = true }: ActionVideoProps) {
const videoRef = useRef<HTMLVideoElement>(null)
const startTimeRef = useRef(0)
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const resolvedSrc = getAssetUrl(src)
const handleClick = () => {
if (enableLightbox) {
startTimeRef.current = videoRef.current?.currentTime ?? 0
setIsLightboxOpen(true)
}
}
@@ -62,6 +65,7 @@ export function ActionVideo({ src, alt, enableLightbox = true }: ActionVideoProp
return (
<>
<video
ref={videoRef}
src={resolvedSrc}
autoPlay
loop
@@ -80,6 +84,7 @@ export function ActionVideo({ src, alt, enableLightbox = true }: ActionVideoProp
src={src}
alt={alt}
type='video'
startTime={startTimeRef.current}
/>
)}
</>

View File

@@ -1,195 +0,0 @@
import { memo } from 'react'
const RX = '2.59574'
interface BlockRect {
opacity: number
width: string
height: string
fill: string
x?: string
y?: string
transform?: string
}
const RECTS = {
topRight: [
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{
opacity: 1,
x: '51.6188',
y: '16.8626',
width: '16.8626',
height: '16.8626',
fill: '#2ABBF8',
},
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#00F701' },
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#00F701' },
{
opacity: 1,
x: '123.6484',
y: '16.8626',
width: '16.8626',
height: '16.8626',
fill: '#00F701',
},
{ opacity: 0.6, x: '157.371', y: '0', width: '34.2403', height: '16.8626', fill: '#FFCC02' },
{ opacity: 1, x: '157.371', y: '0', width: '16.8626', height: '16.8626', fill: '#FFCC02' },
{ opacity: 0.6, x: '208.993', y: '0', width: '68.4805', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 0.6, x: '209.137', y: '0', width: '16.8626', height: '33.7252', fill: '#FA4EDF' },
{ opacity: 0.6, x: '243.233', y: '0', width: '34.2403', height: '33.7252', fill: '#FA4EDF' },
{ opacity: 1, x: '243.233', y: '0', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 0.6, x: '260.096', y: '0', width: '34.04', height: '16.8626', fill: '#FA4EDF' },
{
opacity: 1,
x: '260.611',
y: '16.8626',
width: '16.8626',
height: '16.8626',
fill: '#FA4EDF',
},
],
bottomLeft: [
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{
opacity: 1,
x: '51.6188',
y: '16.8626',
width: '16.8626',
height: '16.8626',
fill: '#2ABBF8',
},
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#00F701' },
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#00F701' },
{
opacity: 1,
x: '123.6484',
y: '16.8626',
width: '16.8626',
height: '16.8626',
fill: '#00F701',
},
],
bottomRight: [
{
opacity: 0.6,
width: '16.8626',
height: '33.726',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 0)',
},
{
opacity: 0.6,
width: '34.241',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 16.891 0)',
},
{
opacity: 0.6,
width: '16.8626',
height: '68.482',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.739 16.888)',
},
{
opacity: 0.6,
width: '16.8626',
height: '33.726',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 33.776)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.739 34.272)',
},
{
opacity: 0.6,
width: '16.8626',
height: '34.24',
fill: '#2ABBF8',
transform: 'matrix(-1 0 0 1 33.787 68)',
},
{
opacity: 0.4,
width: '16.8626',
height: '16.8626',
fill: '#1A8FCC',
transform: 'matrix(-1 0 0 1 33.787 85)',
},
],
} as const satisfies Record<string, readonly BlockRect[]>
const GLOBAL_OPACITY = 0.55
const BlockGroup = memo(function BlockGroup({
width,
height,
viewBox,
rects,
}: {
width: number
height: number
viewBox: string
rects: readonly BlockRect[]
}) {
return (
<svg
width={width}
height={height}
viewBox={viewBox}
fill='none'
xmlns='http://www.w3.org/2000/svg'
className='h-auto w-full'
style={{ opacity: GLOBAL_OPACITY }}
>
{rects.map((r, i) => (
<rect
key={i}
x={r.x}
y={r.y}
width={r.width}
height={r.height}
rx={RX}
fill={r.fill}
transform={r.transform}
opacity={r.opacity}
/>
))}
</svg>
)
})
export function AnimatedBlocks() {
return (
<div
className='pointer-events-none fixed inset-0 z-0 hidden overflow-hidden lg:block'
aria-hidden='true'
>
<div className='absolute top-[93px] right-0 w-[calc(140px+10.76vw)] max-w-[295px]'>
<BlockGroup width={295} height={34} viewBox='0 0 295 34' rects={RECTS.topRight} />
</div>
<div className='-left-24 absolute bottom-0 w-[calc(140px+10.76vw)] max-w-[295px] rotate-180'>
<BlockGroup width={295} height={34} viewBox='0 0 295 34' rects={RECTS.bottomLeft} />
</div>
<div className='-bottom-2 absolute right-0 w-[calc(16px+1.25vw)] max-w-[34px]'>
<BlockGroup width={34} height={102} viewBox='0 0 34 102' rects={RECTS.bottomRight} />
</div>
</div>
)
}

View File

@@ -2,6 +2,7 @@
import { useState } from 'react'
import { ChevronRight } from 'lucide-react'
import { cn } from '@/lib/utils'
interface FAQItem {
question: string
@@ -31,9 +32,10 @@ function FAQItemRow({
className='flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left font-[470] text-[0.875rem] text-[rgba(0,0,0,0.8)] transition-colors hover:bg-[rgba(0,0,0,0.02)] dark:text-[rgba(255,255,255,0.85)] dark:hover:bg-[rgba(255,255,255,0.03)]'
>
<ChevronRight
className={`h-3.5 w-3.5 shrink-0 text-[rgba(0,0,0,0.3)] transition-transform duration-200 dark:text-[rgba(255,255,255,0.3)] ${
isOpen ? 'rotate-90' : ''
}`}
className={cn(
'h-3.5 w-3.5 shrink-0 text-[rgba(0,0,0,0.3)] transition-transform duration-200 dark:text-[rgba(255,255,255,0.3)]',
isOpen && 'rotate-90'
)}
/>
{item.question}
</button>
@@ -81,11 +83,10 @@ export function FAQ({ items, title = 'Common Questions' }: FAQProps) {
{items.map((item, index) => (
<div
key={index}
className={
index !== items.length - 1
? 'border-[rgba(0,0,0,0.08)] border-b dark:border-[rgba(255,255,255,0.08)]'
: ''
}
className={cn(
index !== items.length - 1 &&
'border-[rgba(0,0,0,0.08)] border-b dark:border-[rgba(255,255,255,0.08)]'
)}
>
<FAQItemRow
item={item}

View File

@@ -1,6 +1,5 @@
'use client'
import { useEffect, useState } from 'react'
import { Check } from 'lucide-react'
import { useParams, usePathname, useRouter } from 'next/navigation'
import {
@@ -25,24 +24,9 @@ export function LanguageDropdown() {
const params = useParams()
const router = useRouter()
const [currentLang, setCurrentLang] = useState(() => {
const langFromParams = params?.lang as string
return langFromParams && Object.keys(languages).includes(langFromParams) ? langFromParams : 'en'
})
useEffect(() => {
const langFromParams = params?.lang as string
if (langFromParams && Object.keys(languages).includes(langFromParams)) {
if (langFromParams !== currentLang) {
setCurrentLang(langFromParams)
}
} else {
if (currentLang !== 'en') {
setCurrentLang('en')
}
}
}, [params])
const langFromParams = params?.lang as string
const currentLang =
langFromParams && Object.keys(languages).includes(langFromParams) ? langFromParams : 'en'
const handleLanguageChange = (locale: string) => {
if (locale === currentLang) return

View File

@@ -1,6 +1,6 @@
'use client'
import { useEffect, useRef } from 'react'
import { useEffect, useLayoutEffect, useRef } from 'react'
import { getAssetUrl } from '@/lib/utils'
interface LightboxProps {
@@ -9,10 +9,12 @@ interface LightboxProps {
src: string
alt: string
type: 'image' | 'video'
startTime?: number
}
export function Lightbox({ isOpen, onClose, src, alt, type }: LightboxProps) {
export function Lightbox({ isOpen, onClose, src, alt, type, startTime }: LightboxProps) {
const overlayRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -40,6 +42,12 @@ export function Lightbox({ isOpen, onClose, src, alt, type }: LightboxProps) {
}
}, [isOpen, onClose])
useLayoutEffect(() => {
if (isOpen && type === 'video' && videoRef.current && startTime != null && startTime > 0) {
videoRef.current.currentTime = startTime
}
}, [isOpen, startTime, type])
if (!isOpen) return null
return (
@@ -61,6 +69,7 @@ export function Lightbox({ isOpen, onClose, src, alt, type }: LightboxProps) {
/>
) : (
<video
ref={videoRef}
src={getAssetUrl(src)}
autoPlay
loop

View File

@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
import { getAssetUrl } from '@/lib/utils'
import { useRef, useState } from 'react'
import { cn, getAssetUrl } from '@/lib/utils'
import { Lightbox } from './lightbox'
interface VideoProps {
@@ -12,6 +12,8 @@ interface VideoProps {
muted?: boolean
playsInline?: boolean
enableLightbox?: boolean
width?: number
height?: number
}
export function Video({
@@ -22,11 +24,16 @@ export function Video({
muted = true,
playsInline = true,
enableLightbox = true,
width,
height,
}: VideoProps) {
const videoRef = useRef<HTMLVideoElement>(null)
const startTimeRef = useRef(0)
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const handleVideoClick = () => {
if (enableLightbox) {
startTimeRef.current = videoRef.current?.currentTime ?? 0
setIsLightboxOpen(true)
}
}
@@ -34,11 +41,17 @@ export function Video({
return (
<>
<video
ref={videoRef}
autoPlay={autoPlay}
loop={loop}
muted={muted}
playsInline={playsInline}
className={`${className} ${enableLightbox ? 'cursor-pointer transition-opacity hover:opacity-95' : ''}`}
width={width}
height={height}
className={cn(
className,
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-95'
)}
src={getAssetUrl(src)}
onClick={handleVideoClick}
/>
@@ -50,6 +63,7 @@ export function Video({
src={src}
alt={`Video: ${src}`}
type='video'
startTime={startTimeRef.current}
/>
)}
</>

View File

@@ -5,6 +5,7 @@ description: Your per-workflow AI assistant for building and editing workflows.
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { FAQ } from '@/components/ui/faq'
Copilot is the AI assistant built into every workflow editor. It is scoped to the workflow you have open — it reads the current structure, makes changes directly, and saves checkpoints so you can revert if needed.
@@ -15,7 +16,7 @@ For workspace-wide tasks (managing multiple workflows, running research, working
Copilot is a Sim-managed service. For self-hosted deployments, go to [sim.ai](https://sim.ai) → Settings → Copilot, generate a Copilot API key, then set `COPILOT_API_KEY` in your self-hosted environment.
</Callout>
{/* TODO: Screenshot of the workflow editor with the Copilot panel open on the right side — showing a conversation with a workflow change applied. Ideally shows a message from the user, a response from Copilot, and the checkpoint icon visible on the message. */}
<Video src="copilot/copilot.mp4" width={700} height={450} />
## What Copilot Can Do

View File

@@ -4,18 +4,17 @@ description: Upload, create, edit, and generate files — documents, presentatio
---
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { FAQ } from '@/components/ui/faq'
Describe a document, presentation, image, or visualization and Mothership creates it — streaming the content live into the resource panel as it writes. Attach any file to your message and Mothership reads it, processes it, and saves it to your workspace.
<Video src="mothership/files-pipeline-deals-summarizer.mp4" width={700} height={450} />
{/* TODO: Screenshot of Mothership with the File Write subagent active — file content streaming into the resource panel in split or preview mode. Shows the live streaming preview experience as a document is being written. */}
Describe a document, presentation, image, or visualization and Mothership creates it — streaming the content live into the resource panel as it writes. Attach any file to your message and Mothership reads it, processes it, and saves it to your workspace.
## Uploading Files to the Workspace
Attach any file directly to your Mothership message — drag it into the input, paste it, or click the attachment icon. Mothership reads the file as context and saves it to your workspace.
{/* TODO: Screenshot of the Mothership input area showing a file attached — e.g., a PDF or image thumbnail visible in the input before sending. */}
Use this to:
- Hand Mothership a document and ask it to process, summarize, or extract data from it
- Upload a CSV and have it create a table from it
@@ -48,6 +47,8 @@ Open a file using `@filename` or the **+** menu, then describe the change:
## Presentations
<Image src="/static/mothership/pptx-example.png" alt="Mothership resource panel showing a generated Mothership-Use-Cases.pptx file open with the title slide and first use case slide visible" width={900} height={500} />
Mothership can generate `.pptx` files:
- "Create a pitch deck for Q3 review — 8 slides covering growth, retention, and roadmap"
@@ -58,8 +59,6 @@ Mothership can generate `.pptx` files:
The file is saved to your workspace and can be downloaded.
{/* TODO: Screenshot of the resource panel with a generated .pptx file open or a download prompt visible, showing the file name and confirming it was saved to the workspace. */}
## Images
Mothership can generate images using AI, and can use an existing image as a reference to guide the output:
@@ -73,7 +72,7 @@ Mothership can generate images using AI, and can use an existing image as a refe
- Attach an existing image to your message, then describe what you want: "Generate a new version of this banner with a blue color scheme instead of green"
- "Create a variation of this diagram with the boxes rearranged horizontally [attach image]"
{/* TODO: Screenshot of the resource panel showing a generated image open as a file tab — ideally with the image rendered in the viewer panel. */}
<Image src="/static/mothership/image-example.png" alt="Mothership resource panel showing a generated hero image of a Mothership-branded blimp flying over San Francisco at golden hour, alongside the chat response linking the file" width={900} height={500} />
Generated images are saved as workspace files.
@@ -85,7 +84,7 @@ Mothership can generate charts and data visualizations from data you describe or
- "Create a line chart of token usage over the past 30 days from this data [paste data]"
- "Generate a pie chart showing the distribution of lead sources from the leads table"
{/* TODO: Screenshot of a chart or visualization rendered in the resource panel as a file. */}
<Image src="/static/mothership/chart-example.png" alt="Mothership resource panel showing a generated chart file with bar charts for backend 5xx errors and error rate over time" width={900} height={500} />
Visualizations are saved as files and rendered in the resource panel.
@@ -104,7 +103,7 @@ Results come back directly in the chat. Ask Mothership to save the output as a f
When a file opens in the resource panel, you can switch between three views:
{/* TODO: Screenshot of the file viewer in the resource panel showing the mode selector (editor/split/preview), ideally in split mode with a markdown file showing raw content on the left and rendered preview on the right. */}
<Video src="mothership/toggle-file-view.mp4" width={700} height={450} />
| Mode | What it shows |
|------|--------------|

View File

@@ -4,11 +4,12 @@ description: Your AI command center. Build and manage your entire workspace in n
---
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { FAQ } from '@/components/ui/faq'
Describe what you want and Mothership handles it. Build a workflow, run research, generate a presentation, query a table, schedule a recurring job, send a Slack message — Mothership knows your entire workspace and takes action directly.
<Video src="mothership/create-workflow.mp4" width={700} height={450} />
{/* TODO: Screenshot or GIF of the full Mothership home page — chat pane on the left with a conversation in progress, resource panel on the right with a workflow or file tab open. Hero shot for the page. */}
Describe what you want and Mothership handles it. Build a workflow, run research, generate a presentation, query a table, schedule a recurring job, send a Slack message — Mothership knows your entire workspace and takes action directly.
## What You Can Do
@@ -44,6 +45,8 @@ For complex tasks, Mothership delegates to specialized subagents automatically.
Bring any workspace object into the conversation via the **+** menu, `@`-mentions, or drag-and-drop from the sidebar. Mothership also opens resources automatically when it creates or modifies them.
<Video src="mothership/context-menu.mp4" width={700} height={450} />
{/* TODO: Screenshot of the resource panel with multiple tabs open — a workflow tab, a table tab, and a file tab — showing different resource types side by side. */}
| What to add | How it appears |
@@ -59,6 +62,8 @@ Bring any workspace object into the conversation via the **+** menu, `@`-mention
Mothership has two panes. On the left: the chat thread, where your messages and Mothership's responses appear. On the right: the resource panel, where workflows, tables, files, and knowledge bases open as tabs. The panel is resizable; tabs are draggable and closeable.
<Video src="mothership/split-view.mp4" width={700} height={450} />
<FAQ items={[
{ question: "How is Mothership different from Copilot?", answer: "Copilot is scoped to a single workflow — it helps you build and edit that workflow. Mothership has access to your entire workspace and can build workflows, manage data, run research, schedule jobs, take actions across integrations, and more." },
{ question: "What model does Mothership use?", answer: "Mothership always uses Claude Opus 4.6. There is no model selector." },

View File

@@ -4,11 +4,12 @@ description: Create, populate, and query knowledge bases from Mothership.
---
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { FAQ } from '@/components/ui/faq'
Create a knowledge base, add documents to it, and query it in plain language — all through conversation. Knowledge bases you create in Mothership are immediately available to Agent blocks in any workflow.
<Video src="mothership/kb.mp4" width={700} height={450} />
{/* TODO: Screenshot of Mothership with a knowledge base open in the resource panel — showing the knowledge base name, document list, and status of indexed documents. */}
Create a knowledge base, add documents to it, and query it in plain language — all through conversation. Knowledge bases you create in Mothership are immediately available to Agent blocks in any workflow.
## Creating Knowledge Bases

View File

@@ -4,11 +4,12 @@ description: Ask Mothership to research anything — it searches, reads, and syn
---
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { FAQ } from '@/components/ui/faq'
Ask Mothership to research anything and it figures out the best approach — searching the web, reading specific pages, crawling sites, looking up technical docs. Just describe what you want to know.
<Video src="mothership/research-agent.mp4" width={700} height={450} />
{/* TODO: Screenshot of the Research subagent section in the Mothership chat — expanded, showing it working through a research task with the final report or answer appearing. Ideally with a file tab open in the resource panel showing the output. */}
Ask Mothership to research anything and it figures out the best approach — searching the web, reading specific pages, crawling sites, looking up technical docs. Just describe what you want to know.
## Asking Questions

View File

@@ -6,9 +6,9 @@ description: Create, query, and manage workspace tables from Mothership.
import { Image } from '@/components/ui/image'
import { FAQ } from '@/components/ui/faq'
Create a table from a description or a CSV, query it in plain language, add or update rows, and export the results — all through conversation. Tables open in the resource panel when created or referenced.
<Image src="/static/mothership/table-example.png" alt="Mothership resource panel showing the pipeline_deals table with company, deal_owner, stage, and amount columns, alongside a chat summary of total pipeline value and breakdown by stage" width={900} height={500} />
{/* TODO: Screenshot of Mothership with a table open in the resource panel — ideally after a query or row operation, showing the table with data populated. */}
Create a table from a description or a CSV, query it in plain language, add or update rows, and export the results — all through conversation. Tables open in the resource panel as soon as they're created or referenced.
## Creating Tables

View File

@@ -5,16 +5,17 @@ description: Schedule recurring jobs, take immediate actions, connect integratio
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { FAQ } from '@/components/ui/faq'
<Video src="mothership/job-create.mp4" width={700} height={450} />
Mothership can act on your behalf right now — send a message, create an issue, call an API — or on a schedule, running a prompt automatically every hour, day, or week. It can also connect integrations, set environment variables, add MCP servers, and create custom tools.
## Scheduled Jobs
A scheduled job is a Mothership task that runs on a cron schedule. On each run, Mothership reads the current workspace state and executes the job's prompt as if you had just sent it.
{/* TODO: Screenshot of Mothership chat confirming a scheduled job was created — showing the job name, schedule, and what it will do. If there's a jobs list view in the sidebar, include that as a second screenshot here. */}
### Creating a Job
Describe the recurring task and how often it should run:

View File

@@ -3,13 +3,13 @@ title: Workflows
description: Create, edit, run, debug, deploy, and organize workflows from Mothership.
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { FAQ } from '@/components/ui/faq'
Describe a workflow and Mothership builds it. Reference an existing one by name and it edits it. No canvas navigation required — every change appears in the resource panel in real time.
<Video src="mothership/create-workflow.mp4" width={700} height={450} />
{/* TODO: Screenshot of Mothership chat on the left with the Build subagent section visible, and a workflow open in the resource panel on the right. Shows the split-pane experience of building via natural language. */}
Describe a workflow and Mothership builds it. Reference an existing one by name and it edits it. No canvas navigation required — every change appears in the resource panel in real time.
## Creating Workflows
@@ -33,7 +33,7 @@ Open an existing workflow with `@workflow-name` or the **+** menu, then describe
## Running Workflows
{/* TODO: Screenshot or GIF of Mothership running a workflow — showing the chat streaming execution output on the left while the workflow canvas in the resource panel highlights blocks as they execute in real time. */}
<Video src="mothership/run-workflow.mp4" width={700} height={450} />
Ask Mothership to run a workflow and it handles the execution:
@@ -110,10 +110,6 @@ Variables set this way are available via `<variable.VARIABLE_NAME>` syntax insid
- "Delete the old_api_prototype workflow"
- "Delete all workflows in the deprecated folder"
<Callout type="warn">
Workflow deletion is permanent. Deployed versions are also removed. There is no recycle bin.
</Callout>
<FAQ items={[
{ question: "Can Mothership edit a workflow while it's deployed?", answer: "Yes. Editing a workflow does not affect the live deployment. The deployed version is a snapshot — you need to ask Mothership to redeploy to push changes to production." },
{ question: "Can I run a workflow with specific inputs from Mothership?", answer: "Yes. Describe the inputs in your message and Mothership passes them to the workflow's start block." },

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

View File

@@ -12,6 +12,7 @@ import type {
} from '@/lib/academy/types'
import { validateExercise } from '@/lib/academy/validation'
import { cn } from '@/lib/core/utils/cn'
import { sleep } from '@/lib/core/utils/helpers'
import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
@@ -323,7 +324,7 @@ export function SandboxCanvasProvider({
for (let i = 0; i < plan.length; i++) {
const step = plan[i]
setActiveBlocks(workflowId, new Set([step.blockId]))
await new Promise((resolve) => setTimeout(resolve, step.delay))
await sleep(step.delay)
addConsole({
workflowId,
blockId: step.blockId,

View File

@@ -4,6 +4,7 @@ import { account, credential, credentialSetMember } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, desc, eq, inArray } from 'drizzle-orm'
import { decryptSecret } from '@/lib/core/security/encryption'
import { toError } from '@/lib/core/utils/helpers'
import { refreshOAuthToken } from '@/lib/oauth'
import {
getMicrosoftRefreshTokenExpiry,
@@ -331,7 +332,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
return accessToken
} catch (error) {
logger.error(`Error refreshing token for user ${userId}, provider ${providerId}`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
providerId,
userId,
@@ -460,7 +461,7 @@ export async function refreshAccessTokenIfNeeded(
return refreshedToken.accessToken
} catch (error) {
logger.error(`[${requestId}] Error refreshing token for credential`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
providerId: credential.providerId,
credentialId,
@@ -664,7 +665,7 @@ export async function getCredentialsForCredentialSet(
}
} catch (error) {
logger.error(`Failed to refresh token for user ${cred.userId}, provider ${providerId}`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
continue
}

View File

@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { isSameOrigin } from '@/lib/core/utils/validation'
import { processCredentialDraft } from '@/lib/credentials/draft-processor'
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
@@ -113,7 +114,7 @@ export async function GET(request: NextRequest) {
const returnUrl = request.cookies.get('shopify_return_url')?.value
const redirectUrl = returnUrl || `${baseUrl}/workspace`
const redirectUrl = returnUrl && isSameOrigin(returnUrl) ? returnUrl : `${baseUrl}/workspace`
const finalUrl = new URL(redirectUrl)
finalUrl.searchParams.set('shopify_connected', 'true')

View File

@@ -4,6 +4,7 @@ import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { isSameOrigin } from '@/lib/core/utils/validation'
import { getScopesForService } from '@/lib/oauth/utils'
const logger = createLogger('ShopifyAuthorize')
@@ -192,7 +193,7 @@ export async function GET(request: NextRequest) {
path: '/',
})
if (returnUrl) {
if (returnUrl && isSameOrigin(returnUrl)) {
response.cookies.set('shopify_return_url', returnUrl, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',

View File

@@ -3,6 +3,7 @@ import { headers } from 'next/headers'
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { isAuthDisabled } from '@/lib/core/config/feature-flags'
import { toError } from '@/lib/core/utils/helpers'
const logger = createLogger('SocketTokenAPI')
@@ -36,7 +37,7 @@ export async function POST() {
}
logger.error('Failed to generate socket token', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 })

View File

@@ -147,6 +147,32 @@ export async function POST(request: NextRequest) {
oidcConfig.userInfoEndpoint = userInfoEndpoint
oidcConfig.jwksEndpoint = jwksEndpoint
const userProvidedEndpoints: Record<string, string | undefined> = {
authorizationEndpoint,
tokenEndpoint,
userInfoEndpoint,
jwksEndpoint,
}
for (const [name, endpointUrl] of Object.entries(userProvidedEndpoints)) {
if (endpointUrl) {
const endpointValidation = await validateUrlWithDNS(endpointUrl, `OIDC ${name}`)
if (!endpointValidation.isValid) {
logger.warn('Explicitly provided OIDC endpoint failed SSRF validation', {
endpoint: name,
url: endpointUrl,
error: endpointValidation.error,
})
return NextResponse.json(
{
error: `OIDC ${name} failed security validation: ${endpointValidation.error}`,
},
{ status: 400 }
)
}
}
}
const needsDiscovery =
!oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint

View File

@@ -17,6 +17,7 @@ import {
hasUsableSubscriptionStatus,
} from '@/lib/billing/subscriptions/utils'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { toError } from '@/lib/core/utils/helpers'
import { captureServerEvent } from '@/lib/posthog/server'
const logger = createLogger('SwitchPlan')
@@ -185,7 +186,7 @@ export async function POST(request: NextRequest) {
} catch (error) {
logger.error('Failed to switch subscription', {
userId: session?.user?.id,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to switch plan' },

View File

@@ -7,6 +7,7 @@ import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { checkInternalApiKey } from '@/lib/copilot/request/http'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { type AtomicClaimResult, billingIdempotency } from '@/lib/core/idempotency/service'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
const logger = createLogger('BillingUpdateCostAPI')
@@ -170,7 +171,7 @@ export async function POST(req: NextRequest) {
const duration = Date.now() - startTime
logger.error(`[${requestId}] Cost update failed`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
duration,
})
@@ -180,7 +181,7 @@ export async function POST(req: NextRequest) {
.release(claim.normalizedKey, claim.storageMethod)
.catch((releaseErr) => {
logger.warn(`[${requestId}] Failed to release idempotency claim`, {
error: releaseErr instanceof Error ? releaseErr.message : String(releaseErr),
error: toError(releaseErr).message,
normalizedKey: claim?.normalizedKey,
})
})

View File

@@ -5,6 +5,7 @@ import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
import { abortActiveStream, waitForPendingChatStream } from '@/lib/copilot/request/session'
import { env } from '@/lib/core/config/env'
import { toError } from '@/lib/core/utils/helpers'
const logger = createLogger('CopilotChatAbortAPI')
const GO_EXPLICIT_ABORT_TIMEOUT_MS = 3000
@@ -20,7 +21,7 @@ export async function POST(request: Request) {
const body = await request.json().catch((err) => {
logger.warn('Abort request body parse failed; continuing with empty object', {
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return {}
})
@@ -35,7 +36,7 @@ export async function POST(request: Request) {
const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => {
logger.warn('getLatestRunForStream failed while resolving chatId for abort', {
streamId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
})
@@ -70,7 +71,7 @@ export async function POST(request: Request) {
} catch (err) {
logger.warn('Explicit abort marker request failed; proceeding with local abort', {
streamId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
}

View File

@@ -16,6 +16,7 @@ import {
import { readFilePreviewSessions } from '@/lib/copilot/request/session'
import { readEvents } from '@/lib/copilot/request/session/buffer'
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
import { toError } from '@/lib/core/utils/helpers'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
@@ -82,7 +83,7 @@ export async function GET(req: NextRequest) {
logger.warn('Failed to read preview sessions for copilot chat', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return []
}),
@@ -90,7 +91,7 @@ export async function GET(req: NextRequest) {
logger.warn('Failed to fetch latest run for copilot chat snapshot', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return null
}),
@@ -110,7 +111,7 @@ export async function GET(req: NextRequest) {
logger.warn('Failed to load copilot chat stream snapshot', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
}
}

View File

@@ -15,6 +15,7 @@ import {
SSE_RESPONSE_HEADERS,
} from '@/lib/copilot/request/session'
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
import { sleep, toError } from '@/lib/core/utils/helpers'
export const maxDuration = 3600
@@ -97,7 +98,7 @@ export async function GET(request: NextRequest) {
const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => {
logger.warn('Failed to fetch latest run for stream', {
streamId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
})
@@ -119,7 +120,7 @@ export async function GET(request: NextRequest) {
readFilePreviewSessions(streamId).catch((error) => {
logger.warn('Failed to read preview sessions for stream batch', {
streamId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return []
}),
@@ -235,7 +236,7 @@ export async function GET(request: NextRequest) {
(err) => {
logger.warn('Failed to poll latest run for stream', {
streamId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
}
@@ -273,7 +274,7 @@ export async function GET(request: NextRequest) {
break
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
await sleep(POLL_INTERVAL_MS)
}
if (!controllerClosed && Date.now() - startTime >= MAX_STREAM_MS) {
emitTerminalIfMissing(MothershipStreamV1CompletionStatus.error, {
@@ -286,7 +287,7 @@ export async function GET(request: NextRequest) {
if (!controllerClosed && !request.signal.aborted) {
logger.warn('Stream replay failed', {
streamId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
emitTerminalIfMissing(MothershipStreamV1CompletionStatus.error, {
message: 'The stream replay failed before completion.',

View File

@@ -22,6 +22,7 @@ import {
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { toError } from '@/lib/core/utils/helpers'
const logger = createLogger('CopilotConfirmAPI')
@@ -106,7 +107,7 @@ async function updateToolCallStatus(
logger.error('Failed to update tool call status', {
toolCallId,
status,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return false
}
@@ -133,7 +134,7 @@ export async function POST(req: NextRequest) {
const existing = await getAsyncToolCall(toolCallId).catch((err) => {
logger.warn('Failed to fetch async tool call', {
toolCallId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
})
@@ -145,7 +146,7 @@ export async function POST(req: NextRequest) {
const run = await getRunSegment(existing.runId).catch((err) => {
logger.warn('Failed to fetch run segment', {
runId: existing.runId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
import { toError } from '@/lib/core/utils/helpers'
interface AvailableModel {
id: string
@@ -76,7 +77,7 @@ export async function GET(_req: NextRequest) {
return NextResponse.json({ success: true, models })
} catch (error) {
logger.error('Error fetching available models', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return NextResponse.json(
{

View File

@@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { JOB_RETENTION_HOURS, JOB_STATUS } from '@/lib/core/async-jobs'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import { toError } from '@/lib/core/utils/helpers'
const logger = createLogger('CleanupStaleExecutions')
@@ -73,7 +74,7 @@ export async function GET(request: NextRequest) {
cleaned++
} catch (error) {
logger.error(`Failed to clean up execution ${execution.executionId}:`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
failed++
}
@@ -104,7 +105,7 @@ export async function GET(request: NextRequest) {
}
} catch (error) {
logger.error('Failed to clean up stale async jobs:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
}
@@ -131,7 +132,7 @@ export async function GET(request: NextRequest) {
}
} catch (error) {
logger.error('Failed to clean up stale pending jobs:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
}
@@ -158,7 +159,7 @@ export async function GET(request: NextRequest) {
}
} catch (error) {
logger.error('Failed to delete old async jobs:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
}

View File

@@ -75,10 +75,12 @@ vi.mock('@/lib/uploads/utils/file-utils', () => ({
vi.mock('@/lib/uploads/setup.server', () => ({}))
vi.mock('@/lib/execution/doc-vm', () => ({
generatePdfFromCode: vi.fn().mockResolvedValue(Buffer.from('%PDF-compiled')),
generateDocxFromCode: vi.fn().mockResolvedValue(Buffer.from('PK\x03\x04compiled')),
generatePptxFromCode: vi.fn().mockResolvedValue(Buffer.from('PK\x03\x04compiled')),
vi.mock('@/lib/execution/sandbox/run-task', () => ({
runSandboxTask: vi
.fn()
.mockImplementation(async (taskId: string) =>
taskId === 'pdf-generate' ? Buffer.from('%PDF-compiled') : Buffer.from('PK\x03\x04compiled')
),
}))
vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({

View File

@@ -4,11 +4,7 @@ import { createLogger } from '@sim/logger'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import {
generateDocxFromCode,
generatePdfFromCode,
generatePptxFromCode,
} from '@/lib/execution/doc-vm'
import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads'
import type { StorageContext } from '@/lib/uploads/config'
import { parseWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
@@ -22,6 +18,7 @@ import {
findLocalFile,
getContentType,
} from '@/app/api/files/utils'
import type { SandboxTaskId } from '@/sandbox-tasks/registry'
const logger = createLogger('FilesServeAPI')
@@ -30,24 +27,24 @@ const PDF_MAGIC = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d]) // %PDF-
interface CompilableFormat {
magic: Buffer
compile: (code: string, workspaceId: string) => Promise<Buffer>
taskId: SandboxTaskId
contentType: string
}
const COMPILABLE_FORMATS: Record<string, CompilableFormat> = {
'.pptx': {
magic: ZIP_MAGIC,
compile: generatePptxFromCode,
taskId: 'pptx-generate',
contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
},
'.docx': {
magic: ZIP_MAGIC,
compile: generateDocxFromCode,
taskId: 'docx-generate',
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
},
'.pdf': {
magic: PDF_MAGIC,
compile: generatePdfFromCode,
taskId: 'pdf-generate',
contentType: 'application/pdf',
},
}
@@ -65,8 +62,10 @@ function compiledCacheSet(key: string, buffer: Buffer): void {
async function compileDocumentIfNeeded(
buffer: Buffer,
filename: string,
workspaceId?: string,
raw?: boolean
workspaceId: string | undefined,
raw: boolean,
ownerKey: string | undefined,
signal: AbortSignal | undefined
): Promise<{ buffer: Buffer; contentType: string }> {
if (raw) return { buffer, contentType: getContentType(filename) }
@@ -90,7 +89,11 @@ async function compileDocumentIfNeeded(
return { buffer: cached, contentType: format.contentType }
}
const compiled = await format.compile(code, workspaceId || '')
const compiled = await runSandboxTask(
format.taskId,
{ code, workspaceId: workspaceId || '' },
{ ownerKey, signal }
)
compiledCacheSet(cacheKey, compiled)
return { buffer: compiled, contentType: format.contentType }
}
@@ -153,10 +156,10 @@ export async function GET(
const userId = authResult.userId
if (isUsingCloudStorage()) {
return await handleCloudProxy(cloudKey, userId, raw)
return await handleCloudProxy(cloudKey, userId, raw, request.signal)
}
return await handleLocalFile(cloudKey, userId, raw)
return await handleLocalFile(cloudKey, userId, raw, request.signal)
} catch (error) {
logger.error('Error serving file:', error)
@@ -171,8 +174,10 @@ export async function GET(
async function handleLocalFile(
filename: string,
userId: string,
raw: boolean
raw: boolean,
signal: AbortSignal | undefined
): Promise<NextResponse> {
const ownerKey = `user:${userId}`
try {
const contextParam: StorageContext | undefined = inferContextFromKey(filename) as
| StorageContext
@@ -205,7 +210,9 @@ async function handleLocalFile(
rawBuffer,
displayName,
workspaceId,
raw
raw,
ownerKey,
signal
)
logger.info('Local file served', { userId, filename, size: fileBuffer.length })
@@ -225,8 +232,10 @@ async function handleLocalFile(
async function handleCloudProxy(
cloudKey: string,
userId: string,
raw = false
raw = false,
signal: AbortSignal | undefined = undefined
): Promise<NextResponse> {
const ownerKey = `user:${userId}`
try {
const context = inferContextFromKey(cloudKey)
logger.info(`Inferred context: ${context} from key pattern: ${cloudKey}`)
@@ -262,7 +271,9 @@ async function handleCloudProxy(
rawBuffer,
displayName,
workspaceId,
raw
raw,
ownerKey,
signal
)
logger.info('Cloud file served', {

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getJobQueue } from '@/lib/core/async-jobs'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { createErrorResponse } from '@/app/api/workflows/utils'
@@ -70,7 +71,7 @@ export async function GET(
return NextResponse.json(response)
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
logger.error(`[${requestId}] Error fetching task status:`, error)
if (errorMessage?.includes('not found')) {

View File

@@ -26,6 +26,7 @@ import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context'
import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions'
import { env } from '@/lib/core/config/env'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { toError } from '@/lib/core/utils/helpers'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import {
@@ -231,7 +232,7 @@ class NextResponseCapture {
try {
handler()
} catch (error) {
this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error)))
this.triggerErrorHandlers(toError(error))
}
}
}
@@ -290,7 +291,7 @@ class NextResponseCapture {
try {
this._controller.enqueue(normalized)
} catch (error) {
this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error)))
this.triggerErrorHandlers(toError(error))
}
} else {
this._pendingChunks.push(normalized)
@@ -311,7 +312,7 @@ class NextResponseCapture {
try {
this._controller.close()
} catch (error) {
this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error)))
this.triggerErrorHandlers(toError(error))
}
}
@@ -659,7 +660,7 @@ async function handleDirectToolCall(
content: [
{
type: 'text',
text: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`,
text: `Tool execution failed: ${toError(error).message}`,
},
],
isError: true,
@@ -740,7 +741,7 @@ async function handleBuildToolCall(
logger.warn('Failed to generate workspace context for build tool call', {
workflowId: resolved.workflowId,
workspaceId: resolvedWorkspaceId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
}
}
@@ -789,7 +790,7 @@ async function handleBuildToolCall(
content: [
{
type: 'text',
text: `Build failed: ${error instanceof Error ? error.message : String(error)}`,
text: `Build failed: ${toError(error).message}`,
},
],
isError: true,
@@ -880,7 +881,7 @@ async function handleSubagentToolCall(
content: [
{
type: 'text',
text: `Subagent call failed: ${error instanceof Error ? error.message : String(error)}`,
text: `Subagent call failed: ${toError(error).message}`,
},
],
isError: true,

View File

@@ -3,6 +3,7 @@ import { mcpServers, workflow, workflowBlocks } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { toError } from '@/lib/core/utils/helpers'
import { withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
import type { McpServerStatusConfig, McpTool, McpToolSchema } from '@/lib/mcp/types'
@@ -249,11 +250,7 @@ export const POST = withMcpAuth<{ id: string }>('read')(
})
} catch (error) {
logger.error(`[${requestId}] Error refreshing MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to refresh MCP server'),
'Failed to refresh MCP server',
500
)
return createMcpErrorResponse(toError(error), 'Failed to refresh MCP server', 500)
}
}
)

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { toError } from '@/lib/core/utils/helpers'
import {
McpDnsResolutionError,
McpDomainNotAllowedError,
@@ -138,11 +139,7 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
return createMcpSuccessResponse({ server: updatedServer })
} catch (error) {
logger.error(`[${requestId}] Error updating MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to update MCP server'),
'Failed to update MCP server',
500
)
return createMcpErrorResponse(toError(error), 'Failed to update MCP server', 500)
}
}
)

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { toError } from '@/lib/core/utils/helpers'
import { generateId } from '@/lib/core/utils/uuid'
import {
McpDnsResolutionError,
@@ -44,11 +45,7 @@ export const GET = withMcpAuth('read')(
return createMcpSuccessResponse({ servers })
} catch (error) {
logger.error(`[${requestId}] Error listing MCP servers:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to list MCP servers'),
'Failed to list MCP servers',
500
)
return createMcpErrorResponse(toError(error), 'Failed to list MCP servers', 500)
}
}
)
@@ -220,11 +217,7 @@ export const POST = withMcpAuth('write')(
return createMcpSuccessResponse({ serverId }, 201)
} catch (error) {
logger.error(`[${requestId}] Error registering MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to register MCP server'),
'Failed to register MCP server',
500
)
return createMcpErrorResponse(toError(error), 'Failed to register MCP server', 500)
}
}
)
@@ -297,11 +290,7 @@ export const DELETE = withMcpAuth('admin')(
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
} catch (error) {
logger.error(`[${requestId}] Error deleting MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to delete MCP server'),
'Failed to delete MCP server',
500
)
return createMcpErrorResponse(toError(error), 'Failed to delete MCP server', 500)
}
}
)

View File

@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import type { NextRequest } from 'next/server'
import { toError } from '@/lib/core/utils/helpers'
import { McpClient } from '@/lib/mcp/client'
import {
McpDnsResolutionError,
@@ -220,11 +221,7 @@ export const POST = withMcpAuth('write')(
return createMcpSuccessResponse(result, result.success ? 200 : 400)
} catch (error) {
logger.error(`[${requestId}] Error testing MCP server connection:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to test server connection'),
'Failed to test server connection',
500
)
return createMcpErrorResponse(toError(error), 'Failed to test server connection', 500)
}
}
)

View File

@@ -3,6 +3,7 @@ import { workflow, workflowBlocks } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { toError } from '@/lib/core/utils/helpers'
import { withMcpAuth } from '@/lib/mcp/middleware'
import type { McpToolSchema, StoredMcpTool } from '@/lib/mcp/types'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
@@ -70,11 +71,7 @@ export const GET = withMcpAuth('read')(
return createMcpSuccessResponse({ tools: storedTools })
} catch (error) {
logger.error(`[${requestId}] Error fetching stored MCP tools:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to fetch stored MCP tools'),
'Failed to fetch stored MCP tools',
500
)
return createMcpErrorResponse(toError(error), 'Failed to fetch stored MCP tools', 500)
}
}
)

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { toError } from '@/lib/core/utils/helpers'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpPubSub } from '@/lib/mcp/pubsub'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
@@ -63,11 +64,7 @@ export const GET = withMcpAuth<RouteParams>('read')(
return createMcpSuccessResponse({ server, tools })
} catch (error) {
logger.error(`[${requestId}] Error getting workflow MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to get workflow MCP server'),
'Failed to get workflow MCP server',
500
)
return createMcpErrorResponse(toError(error), 'Failed to get workflow MCP server', 500)
}
}
)
@@ -146,11 +143,7 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
return createMcpSuccessResponse({ server: updatedServer })
} catch (error) {
logger.error(`[${requestId}] Error updating workflow MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to update workflow MCP server'),
'Failed to update workflow MCP server',
500
)
return createMcpErrorResponse(toError(error), 'Failed to update workflow MCP server', 500)
}
}
)
@@ -201,11 +194,7 @@ export const DELETE = withMcpAuth<RouteParams>('admin')(
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
} catch (error) {
logger.error(`[${requestId}] Error deleting workflow MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to delete workflow MCP server'),
'Failed to delete workflow MCP server',
500
)
return createMcpErrorResponse(toError(error), 'Failed to delete workflow MCP server', 500)
}
}
)

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { toError } from '@/lib/core/utils/helpers'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpPubSub } from '@/lib/mcp/pubsub'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
@@ -63,11 +64,7 @@ export const GET = withMcpAuth<RouteParams>('read')(
return createMcpSuccessResponse({ tool })
} catch (error) {
logger.error(`[${requestId}] Error getting tool:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to get tool'),
'Failed to get tool',
500
)
return createMcpErrorResponse(toError(error), 'Failed to get tool', 500)
}
}
)
@@ -164,11 +161,7 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
return createMcpSuccessResponse({ tool: updatedTool })
} catch (error) {
logger.error(`[${requestId}] Error updating tool:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to update tool'),
'Failed to update tool',
500
)
return createMcpErrorResponse(toError(error), 'Failed to update tool', 500)
}
}
)
@@ -232,11 +225,7 @@ export const DELETE = withMcpAuth<RouteParams>('write')(
return createMcpSuccessResponse({ message: `Tool ${toolId} deleted successfully` })
} catch (error) {
logger.error(`[${requestId}] Error deleting tool:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to delete tool'),
'Failed to delete tool',
500
)
return createMcpErrorResponse(toError(error), 'Failed to delete tool', 500)
}
}
)

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { toError } from '@/lib/core/utils/helpers'
import { generateId } from '@/lib/core/utils/uuid'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpPubSub } from '@/lib/mcp/pubsub'
@@ -72,11 +73,7 @@ export const GET = withMcpAuth<RouteParams>('read')(
return createMcpSuccessResponse({ tools })
} catch (error) {
logger.error(`[${requestId}] Error listing tools:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to list tools'),
'Failed to list tools',
500
)
return createMcpErrorResponse(toError(error), 'Failed to list tools', 500)
}
}
)
@@ -237,11 +234,7 @@ export const POST = withMcpAuth<RouteParams>('write')(
return createMcpSuccessResponse({ tool }, 201)
} catch (error) {
logger.error(`[${requestId}] Error adding tool:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to add tool'),
'Failed to add tool',
500
)
return createMcpErrorResponse(toError(error), 'Failed to add tool', 500)
}
}
)

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { toError } from '@/lib/core/utils/helpers'
import { generateId } from '@/lib/core/utils/uuid'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpPubSub } from '@/lib/mcp/pubsub'
@@ -82,11 +83,7 @@ export const GET = withMcpAuth('read')(
return createMcpSuccessResponse({ servers: serversWithToolNames })
} catch (error) {
logger.error(`[${requestId}] Error listing workflow MCP servers:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to list workflow MCP servers'),
'Failed to list workflow MCP servers',
500
)
return createMcpErrorResponse(toError(error), 'Failed to list workflow MCP servers', 500)
}
}
)
@@ -221,11 +218,7 @@ export const POST = withMcpAuth('write')(
return createMcpSuccessResponse({ server, addedTools }, 201)
} catch (error) {
logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to create workflow MCP server'),
'Failed to create workflow MCP server',
500
)
return createMcpErrorResponse(toError(error), 'Failed to create workflow MCP server', 500)
}
}
)

View File

@@ -19,6 +19,7 @@ import { readEvents } from '@/lib/copilot/request/session/buffer'
import { readFilePreviewSessions } from '@/lib/copilot/request/session/file-preview-session'
import { type StreamBatchEvent, toStreamBatchEvent } from '@/lib/copilot/request/session/types'
import { taskPubSub } from '@/lib/copilot/tasks'
import { toError } from '@/lib/core/utils/helpers'
import { captureServerEvent } from '@/lib/posthog/server'
const logger = createLogger('MothershipChatAPI')
@@ -66,7 +67,7 @@ export async function GET(
logger.warn('Failed to read preview sessions for mothership chat', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return []
}),
@@ -75,7 +76,7 @@ export async function GET(
logger.warn('Failed to fetch latest run for mothership chat snapshot', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return null
})
@@ -90,7 +91,7 @@ export async function GET(
logger.warn('Failed to read stream snapshot for mothership chat', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
}
}

View File

@@ -6,6 +6,7 @@ import { buildIntegrationToolSchemas } from '@/lib/copilot/chat/payload'
import { generateWorkspaceContext } from '@/lib/copilot/chat/workspace-context'
import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless'
import { requestExplicitStreamAbort } from '@/lib/copilot/request/session/explicit-abort'
import { toError } from '@/lib/core/utils/helpers'
import { generateId } from '@/lib/core/utils/uuid'
import {
assertActiveWorkspaceAccess,
@@ -110,7 +111,7 @@ export async function POST(req: NextRequest) {
chatId: effectiveChatId,
}).catch((error) => {
reqLogger.warn('Failed to send explicit abort for mothership execution', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
})
}

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import {
@@ -112,7 +113,7 @@ export async function POST(request: NextRequest) {
logger.error(`[${requestId}] Failed to resolve Vertex credential:`, {
provider,
model,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
hasVertexCredential: !!vertexCredential,
})
return NextResponse.json(
@@ -258,17 +259,14 @@ export async function POST(request: NextRequest) {
} catch (error) {
const executionTime = Date.now() - startTime
logger.error(`[${requestId}] Provider request failed:`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
errorName: error instanceof Error ? error.name : 'Unknown',
errorStack: error instanceof Error ? error.stack : undefined,
executionTime,
timestamp: new Date().toISOString(),
})
return NextResponse.json(
{ error: error instanceof Error ? error.message : String(error) },
{ status: 500 }
)
return NextResponse.json({ error: toError(error).message }, { status: 500 })
}
}

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { AuthType } from '@/lib/auth/hybrid'
import { getJobQueue } from '@/lib/core/async-jobs'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -235,7 +236,7 @@ export async function POST(
})
} catch (dispatchError) {
logger.error('Failed to dispatch async resume execution', {
error: dispatchError instanceof Error ? dispatchError.message : String(dispatchError),
error: toError(dispatchError).message,
resumeExecutionId: enqueueResult.resumeExecutionId,
})
return NextResponse.json(

View File

@@ -4,6 +4,7 @@ import { and, eq, isNull, lt, lte, ne, not, or, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import {
@@ -136,7 +137,7 @@ export async function GET(request: NextRequest) {
const output = await executeScheduleJob(payload)
await jobQueue.completeJob(jobId, output)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
logger.error(
`[${requestId}] Schedule execution failed for workflow ${schedule.workflowId}`,
{
@@ -191,7 +192,7 @@ export async function GET(request: NextRequest) {
await executeJobInline(payload)
} catch (error) {
logger.error(`[${requestId}] Job execution failed for ${job.id}`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
await releaseScheduleLock(
job.id,

View File

@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import {
@@ -163,7 +164,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
inserted += result.length
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = toError(err).message
logger.warn(`[${requestId}] Append failed mid-import for table ${tableId}`, {
inserted,
total: coerced.length,
@@ -238,7 +239,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
},
})
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = toError(err).message
const isClientError =
message.includes('row limit') ||
message.includes('Schema validation') ||
@@ -251,7 +252,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
throw err
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
const message = toError(error).message
logger.error(`[${requestId}] CSV import into existing table failed:`, error)
const isClientError =

View File

@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import type { RowData } from '@/lib/table'
import { deleteRow, updateRow } from '@/lib/table'
@@ -193,7 +194,7 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
)
}
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
if (errorMessage === 'Row not found') {
return NextResponse.json({ error: errorMessage }, { status: 404 })
@@ -260,7 +261,7 @@ export async function DELETE(request: NextRequest, { params }: RowRouteParams) {
)
}
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
if (errorMessage === 'Row not found') {
return NextResponse.json({ error: errorMessage }, { status: 404 })

View File

@@ -5,6 +5,7 @@ import { and, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import type { Filter, RowData, Sort, TableSchema } from '@/lib/table'
import {
@@ -181,7 +182,7 @@ async function handleBatchInsert(
},
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
if (
errorMessage.includes('row limit') ||
@@ -289,7 +290,7 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
)
}
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
if (
errorMessage.includes('row limit') ||
@@ -516,7 +517,7 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
)
}
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
if (
errorMessage.includes('Row size exceeds') ||
@@ -616,7 +617,7 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
)
}
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
if (errorMessage.includes('Filter is required')) {
return NextResponse.json({ error: errorMessage }, { status: 400 })
@@ -685,7 +686,7 @@ export async function PATCH(request: NextRequest, { params }: TableRowsRoutePara
)
}
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
if (
errorMessage.includes('Row size exceeds') ||

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import type { RowData } from '@/lib/table'
import { upsertRow } from '@/lib/table'
@@ -87,7 +88,7 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
)
}
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
// Service layer throws descriptive errors for validation/capacity issues
if (

View File

@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import {
@@ -124,7 +125,7 @@ export async function POST(request: NextRequest) {
throw insertError
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
const message = toError(error).message
logger.error(`[${requestId}] CSV import failed:`, error)
const isClientError =

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalApiKey } from '@/lib/copilot/request/http'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
@@ -97,7 +98,7 @@ export async function GET(request: NextRequest) {
}
} catch (error) {
logger.error(`[${requestId}] Error sanitizing template ${template.id}`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return null
}
@@ -112,7 +113,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json(response)
} catch (error) {
logger.error(`[${requestId}] Error fetching approved sanitized templates`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(

View File

@@ -5,6 +5,7 @@ import { z } from 'zod'
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
@@ -89,7 +90,7 @@ export async function POST(request: NextRequest) {
parts.push(dataPart)
} catch (parseError) {
logger.warn(`[${requestId}] Failed to parse data as JSON, skipping DataPart`, {
error: parseError instanceof Error ? parseError.message : String(parseError),
error: toError(parseError).message,
})
}
}

View File

@@ -0,0 +1,134 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { agiloftLogin, agiloftLogout, buildRetrieveAttachmentUrl } from '@/tools/agiloft/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftRetrieveAPI')
const AgiloftRetrieveSchema = z.object({
instanceUrl: z.string().min(1, 'Instance URL is required'),
knowledgeBase: z.string().min(1, 'Knowledge base is required'),
login: z.string().min(1, 'Login is required'),
password: z.string().min(1, 'Password is required'),
table: z.string().min(1, 'Table is required'),
recordId: z.string().min(1, 'Record ID is required'),
fieldName: z.string().min(1, 'Field name is required'),
position: z.string().min(1, 'Position is required'),
})
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Agiloft retrieve attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const body = await request.json()
const data = AgiloftRetrieveSchema.parse(body)
const urlValidation = await validateUrlWithDNS(data.instanceUrl, 'instanceUrl')
if (!urlValidation.isValid) {
logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, {
instanceUrl: data.instanceUrl,
})
return NextResponse.json(
{ success: false, error: urlValidation.error || 'Invalid instance URL' },
{ status: 400 }
)
}
const token = await agiloftLogin(data)
const base = data.instanceUrl.replace(/\/$/, '')
try {
const url = buildRetrieveAttachmentUrl(base, data)
logger.info(`[${requestId}] Downloading attachment from Agiloft`, {
recordId: data.recordId,
fieldName: data.fieldName,
position: data.position,
})
const agiloftResponse = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!agiloftResponse.ok) {
const errorText = await agiloftResponse.text()
logger.error(
`[${requestId}] Agiloft retrieve error: ${agiloftResponse.status} - ${errorText}`
)
return NextResponse.json(
{ success: false, error: `Agiloft error: ${agiloftResponse.status} - ${errorText}` },
{ status: agiloftResponse.status }
)
}
const contentType = agiloftResponse.headers.get('content-type') || 'application/octet-stream'
const contentDisposition = agiloftResponse.headers.get('content-disposition')
let fileName = 'attachment'
if (contentDisposition) {
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
if (match?.[1]) {
fileName = match[1].replace(/['"]/g, '')
}
}
const arrayBuffer = await agiloftResponse.arrayBuffer()
const fileBuffer = Buffer.from(arrayBuffer)
logger.info(`[${requestId}] Attachment downloaded successfully`, {
name: fileName,
size: fileBuffer.length,
mimeType: contentType,
})
const base64Data = fileBuffer.toString('base64')
return NextResponse.json({
success: true,
output: {
file: {
name: fileName,
mimeType: contentType,
data: base64Data,
size: fileBuffer.length,
},
},
})
} finally {
await agiloftLogout(data.instanceUrl, data.knowledgeBase, token)
}
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
return NextResponse.json(
{ success: false, error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
logger.error(`[${requestId}] Error retrieving Agiloft attachment:`, error)
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
export const dynamic = 'force-dynamic'
@@ -115,7 +116,7 @@ export async function POST(request: NextRequest) {
})
} catch (error: any) {
logger.error('Error creating Asana task:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
export const dynamic = 'force-dynamic'
@@ -114,7 +115,7 @@ export async function PUT(request: NextRequest) {
})
} catch (error: any) {
logger.error('Error updating Asana task:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -6,6 +6,7 @@ import {
type ResultField,
} from '@aws-sdk/client-cloudwatch-logs'
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
import { sleep } from '@/lib/core/utils/helpers'
interface AwsCredentials {
region: string
@@ -79,7 +80,7 @@ export async function pollQueryResults(
throw new Error(`CloudWatch Log Insights query ${status.toLowerCase()}`)
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
await sleep(pollIntervalMs)
}
// Timeout -- fetch one last time for partial results

View File

@@ -5,6 +5,7 @@ import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
const logger = createLogger('ImageProxyAPI')
@@ -83,7 +84,7 @@ export async function GET(request: NextRequest) {
},
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorMessage = toError(error).message
logger.error(`[${requestId}] Image proxy error:`, { error: errorMessage })
return new NextResponse(`Failed to proxy image: ${errorMessage}`, {

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage, toAdf } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
@@ -182,7 +183,7 @@ export async function PUT(request: NextRequest) {
})
} catch (error: any) {
logger.error('Error updating Jira issue:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage, toAdf } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
@@ -225,7 +226,7 @@ export async function POST(request: NextRequest) {
})
} catch (error: any) {
logger.error('Error creating Jira issue:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -7,6 +7,7 @@ import {
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -199,7 +200,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
logger.error('Error in approvals operation:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -112,7 +113,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error adding comment:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -105,7 +106,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching comments:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -157,7 +158,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error with customers operation:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -96,7 +97,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error getting form answers:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -104,7 +105,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error attaching form:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -111,7 +112,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error copying forms:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -96,7 +97,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error deleting form:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -97,7 +98,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error externalising form:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -98,7 +99,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error getting form:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -97,7 +98,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error internalising form:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -96,7 +97,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching issue forms:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -97,7 +98,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error reopening form:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -103,7 +104,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error saving form answers:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -98,7 +99,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching form structure:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -97,7 +98,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error submitting form:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -96,7 +97,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching form templates:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -6,6 +6,7 @@ import {
validateEnum,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -169,7 +170,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
logger.error('Error in organization operation:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -91,7 +92,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching organizations:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -6,6 +6,7 @@ import {
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -174,7 +175,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
logger.error('Error in participants operation:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -100,7 +101,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching queues:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -6,6 +6,7 @@ import {
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -250,7 +251,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error with request operation:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -6,6 +6,7 @@ import {
validateEnum,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -140,7 +141,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching requests:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -108,7 +109,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching request type fields:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -104,7 +105,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching request types:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -82,7 +83,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching service desks:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -92,7 +93,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching SLA info:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -6,6 +6,7 @@ import {
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -116,7 +117,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error transitioning request:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
@@ -92,7 +93,7 @@ export async function POST(request: NextRequest) {
})
} catch (error) {
logger.error('Error fetching transitions:', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -99,7 +100,7 @@ export async function POST(request: Request) {
} catch (innerError) {
logger.error('Error during API requests:', innerError)
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
const errorMessage = toError(innerError).message
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { toError } from '@/lib/core/utils/helpers'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -110,15 +111,13 @@ const getChatDisplayName = async (
}
} catch (error) {
logger.warn(
`Failed to get better name from messages for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`
`Failed to get better name from messages for chat ${chatId}: ${toError(error).message}`
)
}
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
} catch (error) {
logger.warn(
`Failed to get display name for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`
)
logger.warn(`Failed to get display name for chat ${chatId}: ${toError(error).message}`)
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
}
}
@@ -200,7 +199,7 @@ export async function POST(request: Request) {
} catch (innerError) {
logger.error('Error during API requests:', innerError)
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
const errorMessage = toError(innerError).message
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||

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