mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-25 06:48:12 -05:00
Compare commits
50 Commits
v0.5.67
...
python-sdk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d63a5cb504 | ||
|
|
1bf5ed4586 | ||
|
|
dc0ed842c4 | ||
|
|
1952b196a0 | ||
|
|
fa03d4d818 | ||
|
|
e14cebeec5 | ||
|
|
404d8c006e | ||
|
|
8bd5d41723 | ||
|
|
ac91d78834 | ||
|
|
6f0a093869 | ||
|
|
c12931bc50 | ||
|
|
bcf6dc8828 | ||
|
|
841cb638fb | ||
|
|
c7db48e3a2 | ||
|
|
4d844651c2 | ||
|
|
9f916940b3 | ||
|
|
3bbf7f5d1d | ||
|
|
68683258c3 | ||
|
|
fc7f56e21b | ||
|
|
8429040921 | ||
|
|
8574e6c71f | ||
|
|
9c3e663cd8 | ||
|
|
48adaa00d8 | ||
|
|
211a7ac3a4 | ||
|
|
0f9b6ad1d2 | ||
|
|
12100e6881 | ||
|
|
23294683e1 | ||
|
|
b913cff46e | ||
|
|
428781ce7d | ||
|
|
f0ee67f3ed | ||
|
|
f44594c380 | ||
|
|
6464cfa7f2 | ||
|
|
7f4edc85ef | ||
|
|
efef91ece0 | ||
|
|
64efeaa2e6 | ||
|
|
9b72b52b33 | ||
|
|
1467862488 | ||
|
|
7f2262857c | ||
|
|
1b309b50e6 | ||
|
|
f765b83a26 | ||
|
|
aa99db6fdd | ||
|
|
e9c4251c1c | ||
|
|
748793e07d | ||
|
|
91da7e183a | ||
|
|
ab09a5ad23 | ||
|
|
fcd0240db6 | ||
|
|
4e4149792a | ||
|
|
9a8b591257 | ||
|
|
f3ae3f8442 | ||
|
|
66dfe2c6b2 |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -27,10 +27,11 @@ jobs:
|
||||
steps:
|
||||
- name: Extract version from commit message
|
||||
id: extract
|
||||
env:
|
||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
||||
# Only tag versions on main branch
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
||||
if [ "$GITHUB_REF" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
||||
VERSION="${BASH_REMATCH[1]}"
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "is_release=true" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -119,6 +119,19 @@ aside#nd-sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide TOC popover on tablet/medium screens (768px - 1279px) */
|
||||
/* Keeps it visible on mobile (<768px) for easy navigation */
|
||||
/* Desktop (>=1280px) already hides it via fumadocs xl:hidden */
|
||||
@media (min-width: 768px) and (max-width: 1279px) {
|
||||
#nd-docs-layout {
|
||||
--fd-toc-popover-height: 0px !important;
|
||||
}
|
||||
|
||||
[data-toc-popover] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop only: Apply custom navbar offset, sidebar width and margin offsets */
|
||||
/* On mobile, let fumadocs handle the layout natively */
|
||||
@media (min-width: 1024px) {
|
||||
|
||||
@@ -124,11 +124,44 @@ Choose between four types of loops:
|
||||
3. Drag other blocks inside the loop container
|
||||
4. Connect the blocks as needed
|
||||
|
||||
### Accessing Results
|
||||
### Referencing Loop Data
|
||||
|
||||
After a loop completes, you can access aggregated results:
|
||||
There's an important distinction between referencing loop data from **inside** vs **outside** the loop:
|
||||
|
||||
- **`<loop.results>`**: Array of results from all loop iterations
|
||||
<Tabs items={['Inside the Loop', 'Outside the Loop']}>
|
||||
<Tab>
|
||||
**Inside the loop**, use `<loop.>` references to access the current iteration context:
|
||||
|
||||
- **`<loop.index>`**: Current iteration number (0-based)
|
||||
- **`<loop.currentItem>`**: Current item being processed (forEach only)
|
||||
- **`<loop.items>`**: Full collection being iterated (forEach only)
|
||||
|
||||
```
|
||||
// Inside a Function block within the loop
|
||||
const idx = <loop.index>; // 0, 1, 2, ...
|
||||
const item = <loop.currentItem>; // Current item
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
These references are only available for blocks **inside** the loop container. They give you access to the current iteration's context.
|
||||
</Callout>
|
||||
</Tab>
|
||||
<Tab>
|
||||
**Outside the loop** (after it completes), reference the loop block by its name to access aggregated results:
|
||||
|
||||
- **`<LoopBlockName.results>`**: Array of results from all iterations
|
||||
|
||||
```
|
||||
// If your loop block is named "Process Items"
|
||||
const allResults = <processitems.results>;
|
||||
// Returns: [result1, result2, result3, ...]
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
After the loop completes, use the loop's block name (not `loop.`) to access the collected results. The block name is normalized (lowercase, no spaces).
|
||||
</Callout>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
@@ -184,28 +217,29 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
Available **inside** the loop only:
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>loop.currentItem</strong>: Current item being processed
|
||||
<strong>{"<loop.index>"}</strong>: Current iteration number (0-based)
|
||||
</li>
|
||||
<li>
|
||||
<strong>loop.index</strong>: Current iteration number (0-based)
|
||||
<strong>{"<loop.currentItem>"}</strong>: Current item being processed (forEach only)
|
||||
</li>
|
||||
<li>
|
||||
<strong>loop.items</strong>: Full collection (forEach loops)
|
||||
<strong>{"<loop.items>"}</strong>: Full collection (forEach only)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>loop.results</strong>: Array of all iteration results
|
||||
<strong>{"<blockname.results>"}</strong>: Array of all iteration results (accessed via block name)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Structure</strong>: Results maintain iteration order
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the loop
|
||||
<strong>Access</strong>: Available in blocks after the loop completes
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
|
||||
@@ -76,11 +76,44 @@ Choose between two types of parallel execution:
|
||||
3. Drag a single block inside the parallel container
|
||||
4. Connect the block as needed
|
||||
|
||||
### Accessing Results
|
||||
### Referencing Parallel Data
|
||||
|
||||
After a parallel block completes, you can access aggregated results:
|
||||
There's an important distinction between referencing parallel data from **inside** vs **outside** the parallel block:
|
||||
|
||||
- **`<parallel.results>`**: Array of results from all parallel instances
|
||||
<Tabs items={['Inside the Parallel', 'Outside the Parallel']}>
|
||||
<Tab>
|
||||
**Inside the parallel**, use `<parallel.>` references to access the current instance context:
|
||||
|
||||
- **`<parallel.index>`**: Current instance number (0-based)
|
||||
- **`<parallel.currentItem>`**: Item for this instance (collection-based only)
|
||||
- **`<parallel.items>`**: Full collection being distributed (collection-based only)
|
||||
|
||||
```
|
||||
// Inside a Function block within the parallel
|
||||
const idx = <parallel.index>; // 0, 1, 2, ...
|
||||
const item = <parallel.currentItem>; // This instance's item
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
These references are only available for blocks **inside** the parallel container. They give you access to the current instance's context.
|
||||
</Callout>
|
||||
</Tab>
|
||||
<Tab>
|
||||
**Outside the parallel** (after it completes), reference the parallel block by its name to access aggregated results:
|
||||
|
||||
- **`<ParallelBlockName.results>`**: Array of results from all instances
|
||||
|
||||
```
|
||||
// If your parallel block is named "Process Tasks"
|
||||
const allResults = <processtasks.results>;
|
||||
// Returns: [result1, result2, result3, ...]
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
After the parallel completes, use the parallel's block name (not `parallel.`) to access the collected results. The block name is normalized (lowercase, no spaces).
|
||||
</Callout>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
@@ -98,11 +131,11 @@ Parallel (["gpt-4o", "claude-3.7-sonnet", "gemini-2.5-pro"]) → Agent → Evalu
|
||||
|
||||
### Result Aggregation
|
||||
|
||||
Results from all parallel instances are automatically collected:
|
||||
Results from all parallel instances are automatically collected and accessible via the block name:
|
||||
|
||||
```javascript
|
||||
// In a Function block after the parallel
|
||||
const allResults = input.parallel.results;
|
||||
// In a Function block after a parallel named "Process Tasks"
|
||||
const allResults = <processtasks.results>;
|
||||
// Returns: [result1, result2, result3, ...]
|
||||
```
|
||||
|
||||
@@ -158,25 +191,26 @@ Understanding when to use each:
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
Available **inside** the parallel only:
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>parallel.currentItem</strong>: Item for this instance
|
||||
<strong>{"<parallel.index>"}</strong>: Instance number (0-based)
|
||||
</li>
|
||||
<li>
|
||||
<strong>parallel.index</strong>: Instance number (0-based)
|
||||
<strong>{"<parallel.currentItem>"}</strong>: Item for this instance (collection-based only)
|
||||
</li>
|
||||
<li>
|
||||
<strong>parallel.items</strong>: Full collection (collection-based)
|
||||
<strong>{"<parallel.items>"}</strong>: Full collection (collection-based only)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>parallel.results</strong>: Array of all instance results
|
||||
<strong>{"<blockname.results>"}</strong>: Array of all instance results (accessed via block name)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the parallel
|
||||
<strong>Access</strong>: Available in blocks after the parallel completes
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
|
||||
@@ -5,45 +5,25 @@ title: Copilot
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react'
|
||||
import { MessageCircle, Hammer, Zap, Globe, Paperclip, History, RotateCcw, Brain } from 'lucide-react'
|
||||
|
||||
Copilot is your in-editor assistant that helps you build and edit workflows with Sim Copilot, as well as understand and improve them. It can:
|
||||
Copilot is your in-editor assistant that helps you build and edit workflows. It can:
|
||||
|
||||
- **Explain**: Answer questions about Sim and your current workflow
|
||||
- **Guide**: Suggest edits and best practices
|
||||
- **Edit**: Make changes to blocks, connections, and settings when you approve
|
||||
- **Build**: Add blocks, wire connections, and configure settings
|
||||
- **Debug**: Analyze execution issues and optimize performance
|
||||
|
||||
<Callout type="info">
|
||||
Copilot is a Sim-managed service. For self-hosted deployments, generate a Copilot API key in the hosted app (sim.ai → Settings → Copilot)
|
||||
Copilot is a Sim-managed service. For self-hosted deployments:
|
||||
1. Go to [sim.ai](https://sim.ai) → Settings → Copilot and generate a Copilot API key
|
||||
2. Set `COPILOT_API_KEY` in your self-hosted environment to that value
|
||||
2. Set `COPILOT_API_KEY` in your self-hosted environment
|
||||
</Callout>
|
||||
|
||||
## Context Menu (@)
|
||||
|
||||
Use the `@` symbol to reference various resources and give Copilot more context about your workspace:
|
||||
|
||||
<Image
|
||||
src="/static/copilot/copilot-menu.png"
|
||||
alt="Copilot context menu showing available reference options"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
The `@` menu provides access to:
|
||||
- **Chats**: Reference previous copilot conversations
|
||||
- **All workflows**: Reference any workflow in your workspace
|
||||
- **Workflow Blocks**: Reference specific blocks from workflows
|
||||
- **Blocks**: Reference block types and templates
|
||||
- **Knowledge**: Reference your uploaded documents and knowledgebase
|
||||
- **Docs**: Reference Sim documentation
|
||||
- **Templates**: Reference workflow templates
|
||||
- **Logs**: Reference execution logs and results
|
||||
|
||||
This contextual information helps Copilot provide more accurate and relevant assistance for your specific use case.
|
||||
|
||||
## Modes
|
||||
|
||||
Switch between modes using the mode selector at the bottom of the input area.
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title={
|
||||
@@ -60,113 +40,153 @@ This contextual information helps Copilot provide more accurate and relevant ass
|
||||
<Card
|
||||
title={
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-muted-foreground" />
|
||||
Agent
|
||||
<Hammer className="h-4 w-4 text-muted-foreground" />
|
||||
Build
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="m-0 text-sm">
|
||||
Build-and-edit mode. Copilot proposes specific edits (add blocks, wire variables, tweak settings) and applies them when you approve.
|
||||
Workflow building mode. Copilot can add blocks, wire connections, edit configurations, and debug issues.
|
||||
</div>
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/copilot/copilot-mode.png"
|
||||
alt="Copilot mode selection interface"
|
||||
width={600}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
## Models
|
||||
|
||||
## Depth Levels
|
||||
Select your preferred AI model using the model selector at the bottom right of the input area.
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title={
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
Fast
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="m-0 text-sm">Quickest and cheapest. Best for small edits, simple workflows, and minor tweaks.</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<InfinityIcon className="h-4 w-4 text-muted-foreground" />
|
||||
Auto
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="m-0 text-sm">Balanced speed and reasoning. Recommended default for most tasks.</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Brain className="h-4 w-4 text-muted-foreground" />
|
||||
Advanced
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="m-0 text-sm">More reasoning for larger workflows and complex edits while staying performant.</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<BrainCircuit className="h-4 w-4 text-muted-foreground" />
|
||||
Behemoth
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="m-0 text-sm">Maximum reasoning for deep planning, debugging, and complex architectural changes.</div>
|
||||
</Card>
|
||||
</Cards>
|
||||
**Available Models:**
|
||||
- Claude 4.5 Opus, Sonnet (default), Haiku
|
||||
- GPT 5.2 Codex, Pro
|
||||
- Gemini 3 Pro
|
||||
|
||||
### Mode Selection Interface
|
||||
Choose based on your needs: faster models for simple tasks, more capable models for complex workflows.
|
||||
|
||||
You can easily switch between different reasoning modes using the mode selector in the Copilot interface:
|
||||
## Context Menu (@)
|
||||
|
||||
<Image
|
||||
src="/static/copilot/copilot-models.png"
|
||||
alt="Copilot mode selection showing Advanced mode with MAX toggle"
|
||||
width={600}
|
||||
height={300}
|
||||
/>
|
||||
Use the `@` symbol to reference resources and give Copilot more context:
|
||||
|
||||
The interface allows you to:
|
||||
- **Select reasoning level**: Choose from Fast, Auto, Advanced, or Behemoth
|
||||
- **Enable MAX mode**: Toggle for maximum reasoning capabilities when you need the most thorough analysis
|
||||
- **See mode descriptions**: Understand what each mode is optimized for
|
||||
| Reference | Description |
|
||||
|-----------|-------------|
|
||||
| **Chats** | Previous copilot conversations |
|
||||
| **Workflows** | Any workflow in your workspace |
|
||||
| **Workflow Blocks** | Blocks in the current workflow |
|
||||
| **Blocks** | Block types and templates |
|
||||
| **Knowledge** | Uploaded documents and knowledge bases |
|
||||
| **Docs** | Sim documentation |
|
||||
| **Templates** | Workflow templates |
|
||||
| **Logs** | Execution logs and results |
|
||||
|
||||
Choose your mode based on the complexity of your task - use Fast for simple questions and Behemoth for complex architectural changes.
|
||||
Type `@` in the input field to open the context menu, then search or browse to find what you need.
|
||||
|
||||
## Billing and Cost Calculation
|
||||
## Slash Commands (/)
|
||||
|
||||
### How Costs Are Calculated
|
||||
Use slash commands for quick actions:
|
||||
|
||||
Copilot usage is billed per token from the underlying LLM:
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/fast` | Fast mode execution |
|
||||
| `/research` | Research and exploration mode |
|
||||
| `/actions` | Execute agent actions |
|
||||
|
||||
- **Input tokens**: billed at the provider's base rate (**at-cost**)
|
||||
- **Output tokens**: billed at **1.5×** the provider's base output rate
|
||||
**Web Commands:**
|
||||
|
||||
```javascript
|
||||
copilotCost = (inputTokens × inputPrice + outputTokens × (outputPrice × 1.5)) / 1,000,000
|
||||
```
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/search` | Search the web |
|
||||
| `/read` | Read a specific URL |
|
||||
| `/scrape` | Scrape web page content |
|
||||
| `/crawl` | Crawl multiple pages |
|
||||
|
||||
| Component | Rate Applied |
|
||||
|----------|----------------------|
|
||||
| Input | inputPrice |
|
||||
| Output | outputPrice × 1.5 |
|
||||
Type `/` in the input field to see available commands.
|
||||
|
||||
<Callout type="warning">
|
||||
Pricing shown reflects rates as of September 4, 2025. Check provider documentation for current pricing.
|
||||
</Callout>
|
||||
## Chat Management
|
||||
|
||||
### Starting a New Chat
|
||||
|
||||
Click the **+** button in the Copilot header to start a fresh conversation.
|
||||
|
||||
### Chat History
|
||||
|
||||
Click **History** to view previous conversations grouped by date. You can:
|
||||
- Click a chat to resume it
|
||||
- Delete chats you no longer need
|
||||
|
||||
### Editing Messages
|
||||
|
||||
Hover over any of your messages and click **Edit** to modify and resend it. This is useful for refining your prompts.
|
||||
|
||||
### Message Queue
|
||||
|
||||
If you send a message while Copilot is still responding, it gets queued. You can:
|
||||
- View queued messages in the expandable queue panel
|
||||
- Send a queued message immediately (aborts current response)
|
||||
- Remove messages from the queue
|
||||
|
||||
## File Attachments
|
||||
|
||||
Click the attachment icon to upload files with your message. Supported file types include:
|
||||
- Images (preview thumbnails shown)
|
||||
- PDFs
|
||||
- Text files, JSON, XML
|
||||
- Other document formats
|
||||
|
||||
Files are displayed as clickable thumbnails that open in a new tab.
|
||||
|
||||
## Checkpoints & Changes
|
||||
|
||||
When Copilot makes changes to your workflow, it saves checkpoints so you can revert if needed.
|
||||
|
||||
### Viewing Checkpoints
|
||||
|
||||
Hover over a Copilot message and click the checkpoints icon to see saved workflow states for that message.
|
||||
|
||||
### Reverting Changes
|
||||
|
||||
Click **Revert** on any checkpoint to restore your workflow to that state. A confirmation dialog will warn that this action cannot be undone.
|
||||
|
||||
### Accepting Changes
|
||||
|
||||
When Copilot proposes changes, you can:
|
||||
- **Accept**: Apply the proposed changes (`Mod+Shift+Enter`)
|
||||
- **Reject**: Dismiss the changes and keep your current workflow
|
||||
|
||||
## Thinking Blocks
|
||||
|
||||
For complex requests, Copilot may show its reasoning process in expandable thinking blocks:
|
||||
|
||||
- Blocks auto-expand while Copilot is thinking
|
||||
- Click to manually expand/collapse
|
||||
- Shows duration of the thinking process
|
||||
- Helps you understand how Copilot arrived at its solution
|
||||
|
||||
## Options Selection
|
||||
|
||||
When Copilot presents multiple options, you can select using:
|
||||
|
||||
| Control | Action |
|
||||
|---------|--------|
|
||||
| **1-9** | Select option by number |
|
||||
| **Arrow Up/Down** | Navigate between options |
|
||||
| **Enter** | Select highlighted option |
|
||||
|
||||
Selected options are highlighted; unselected options appear struck through.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `@` | Open context menu |
|
||||
| `/` | Open slash commands |
|
||||
| `Arrow Up/Down` | Navigate menu items |
|
||||
| `Enter` | Select menu item |
|
||||
| `Esc` | Close menus |
|
||||
| `Mod+Shift+Enter` | Accept Copilot changes |
|
||||
|
||||
## Usage Limits
|
||||
|
||||
Copilot usage is billed per token from the underlying LLM. If you reach your usage limit, Copilot will prompt you to increase your limit. You can add usage in increments ($50, $100) from your current base.
|
||||
|
||||
<Callout type="info">
|
||||
Model prices are per million tokens. The calculation divides by 1,000,000 to get the actual cost. See <a href="/execution/costs">the Cost Calculation page</a> for background and examples.
|
||||
See the [Cost Calculation page](/execution/costs) for billing details.
|
||||
</Callout>
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ Speed up your workflow building with these keyboard shortcuts and mouse controls
|
||||
| `Mod` + `V` | Paste blocks |
|
||||
| `Delete` or `Backspace` | Delete selected blocks or edges |
|
||||
| `Shift` + `L` | Auto-layout canvas |
|
||||
| `Mod` + `Shift` + `F` | Fit to view |
|
||||
| `Mod` + `Shift` + `Enter` | Accept Copilot changes |
|
||||
|
||||
## Panel Navigation
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"pages": [
|
||||
"./introduction/index",
|
||||
"./getting-started/index",
|
||||
"./quick-reference/index",
|
||||
"triggers",
|
||||
"blocks",
|
||||
"tools",
|
||||
|
||||
136
apps/docs/content/docs/en/quick-reference/index.mdx
Normal file
136
apps/docs/content/docs/en/quick-reference/index.mdx
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: Quick Reference
|
||||
description: Essential actions for navigating and using the Sim workflow editor
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
A quick lookup for everyday actions in the Sim workflow editor. For keyboard shortcuts, see [Keyboard Shortcuts](/keyboard-shortcuts).
|
||||
|
||||
<Callout type="info">
|
||||
**Mod** refers to `Cmd` on macOS and `Ctrl` on Windows/Linux.
|
||||
</Callout>
|
||||
|
||||
## Workspaces
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Create a workspace | Click workspace dropdown in sidebar → **New Workspace** |
|
||||
| Rename a workspace | Workspace settings → Edit name |
|
||||
| Switch workspaces | Click workspace dropdown in sidebar → Select workspace |
|
||||
| Invite team members | Workspace settings → **Team** → **Invite** |
|
||||
|
||||
## Workflows
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Create a workflow | Click **New Workflow** button or `Mod+Shift+A` |
|
||||
| Rename a workflow | Double-click workflow name in sidebar, or right-click → **Rename** |
|
||||
| Duplicate a workflow | Right-click workflow → **Duplicate** |
|
||||
| Reorder workflows | Drag workflow up/down in the sidebar list |
|
||||
| Import a workflow | Sidebar menu → **Import** → Select file |
|
||||
| Create a folder | Right-click in sidebar → **New Folder** |
|
||||
| Rename a folder | Right-click folder → **Rename** |
|
||||
| Delete a folder | Right-click folder → **Delete** |
|
||||
| Collapse/expand folder | Click folder arrow, or double-click folder |
|
||||
| Move workflow to folder | Drag workflow onto folder in sidebar |
|
||||
| Delete a workflow | Right-click workflow → **Delete** |
|
||||
| Export a workflow | Right-click workflow → **Export** |
|
||||
| Assign workflow color | Right-click workflow → **Change Color** |
|
||||
| Multi-select workflows | `Mod+Click` or `Shift+Click` workflows in sidebar |
|
||||
| Open in new tab | Right-click workflow → **Open in New Tab** |
|
||||
|
||||
## Blocks
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Add a block | Drag from Toolbar panel, or right-click canvas → **Add Block** |
|
||||
| Select a block | Click on the block |
|
||||
| Multi-select blocks | `Mod+Click` additional blocks, or right-drag to draw selection box |
|
||||
| Move blocks | Drag selected block(s) to new position |
|
||||
| Copy blocks | `Mod+C` with blocks selected |
|
||||
| Paste blocks | `Mod+V` to paste copied blocks |
|
||||
| Duplicate blocks | Right-click → **Duplicate** |
|
||||
| Delete blocks | `Delete` or `Backspace` key, or right-click → **Delete** |
|
||||
| Rename a block | Click block name in header, or edit in the Editor panel |
|
||||
| Enable/Disable a block | Right-click → **Enable/Disable** |
|
||||
| Toggle handle orientation | Right-click → **Toggle Handles** |
|
||||
| Toggle trigger mode | Right-click trigger block → **Toggle Trigger Mode** |
|
||||
| Configure a block | Select block → use Editor panel on right |
|
||||
|
||||
## Connections
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Create a connection | Drag from output handle to input handle |
|
||||
| Delete a connection | Click edge to select → `Delete` key |
|
||||
| Use output in another block | Drag connection tag into input field |
|
||||
|
||||
## Canvas Navigation
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Pan/move canvas | Left-drag on empty space, or scroll/trackpad |
|
||||
| Zoom in/out | Scroll wheel or pinch gesture |
|
||||
| Auto-layout | `Shift+L` |
|
||||
| Draw selection box | Right-drag on empty canvas area |
|
||||
|
||||
## Panels & Views
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Open Copilot tab | Press `C` or click Copilot tab |
|
||||
| Open Toolbar tab | Press `T` or click Toolbar tab |
|
||||
| Open Editor tab | Press `E` or click Editor tab |
|
||||
| Search toolbar | `Mod+F` |
|
||||
| Toggle advanced mode | Click toggle button on input fields |
|
||||
| Resize panels | Drag panel edge |
|
||||
| Collapse/expand sidebar | Click collapse button on sidebar |
|
||||
|
||||
## Running & Testing
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Run workflow | Click Play button or `Mod+Enter` |
|
||||
| Stop workflow | Click Stop button or `Mod+Enter` while running |
|
||||
| Test with chat | Use Chat panel on the right side |
|
||||
| Select output to view | Click dropdown in Chat panel → Select block output |
|
||||
| Clear chat history | Click clear button in Chat panel |
|
||||
| View execution logs | Open terminal panel at bottom, or `Mod+L` |
|
||||
| Filter logs by block | Click block filter in terminal |
|
||||
| Filter logs by status | Click status filter in terminal |
|
||||
| Search logs | Use search field in terminal |
|
||||
| Copy log entry | Right-click log entry → **Copy** |
|
||||
| Clear terminal | `Mod+D` |
|
||||
|
||||
## Deployment
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Deploy a workflow | Click **Deploy** button in Deploy tab |
|
||||
| Update deployment | Click **Update** when changes are detected |
|
||||
| View deployment status | Check status indicator (Live/Update/Deploy) in Deploy tab |
|
||||
| Revert deployment | Access previous versions in Deploy tab |
|
||||
| Copy webhook URL | Deploy tab → Copy webhook URL |
|
||||
| Copy API endpoint | Deploy tab → Copy API endpoint URL |
|
||||
| Set up a schedule | Add Schedule trigger block → Configure interval |
|
||||
|
||||
## Variables
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Add workflow variable | Variables tab → **Add Variable** |
|
||||
| Edit workflow variable | Variables tab → Click variable to edit |
|
||||
| Delete workflow variable | Variables tab → Click delete icon on variable |
|
||||
| Add environment variable | Settings → **Environment Variables** → **Add** |
|
||||
| Reference a variable | Use `{{variableName}}` syntax in block inputs |
|
||||
|
||||
## Credentials
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Add API key | Block credential field → **Add Credential** → Enter API key |
|
||||
| Connect OAuth account | Block credential field → **Connect** → Authorize with provider |
|
||||
| Manage credentials | Settings → **Credentials** |
|
||||
| Remove credential | Settings → **Credentials** → Delete credential |
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function StatusIndicator() {
|
||||
href={statusUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`flex items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
|
||||
className={`flex min-w-[165px] items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
|
||||
aria-label={`System status: ${message}`}
|
||||
>
|
||||
<StatusDotIcon status={status} className='h-[6px] w-[6px]' aria-hidden='true' />
|
||||
|
||||
@@ -10,8 +10,8 @@ export { LandingLoopNode } from './landing-canvas/landing-block/landing-loop-nod
|
||||
export { LandingNode } from './landing-canvas/landing-block/landing-node'
|
||||
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
||||
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
||||
export type { TagProps } from './landing-canvas/landing-block/tag'
|
||||
export { Tag } from './landing-canvas/landing-block/tag'
|
||||
export type { SubBlockRowProps, TagProps } from './landing-canvas/landing-block/tag'
|
||||
export { SubBlockRow, Tag } from './landing-canvas/landing-block/tag'
|
||||
export type {
|
||||
LandingBlockNode,
|
||||
LandingCanvasProps,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import { BookIcon } from 'lucide-react'
|
||||
import {
|
||||
Tag,
|
||||
type TagProps,
|
||||
SubBlockRow,
|
||||
type SubBlockRowProps,
|
||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
||||
|
||||
/**
|
||||
* Data structure for a landing card component
|
||||
* Matches the workflow block structure from the application
|
||||
*/
|
||||
export interface LandingCardData {
|
||||
/** Icon element to display in the card header */
|
||||
@@ -15,8 +15,8 @@ export interface LandingCardData {
|
||||
color: string | '#f6f6f6'
|
||||
/** Name/title of the card */
|
||||
name: string
|
||||
/** Optional tags to display at the bottom of the card */
|
||||
tags?: TagProps[]
|
||||
/** Optional subblock rows to display below the header */
|
||||
tags?: SubBlockRowProps[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,8 @@ export interface LandingBlockProps extends LandingCardData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Landing block component that displays a card with icon, name, and optional tags
|
||||
* Landing block component that displays a card with icon, name, and optional subblock rows
|
||||
* Styled to match the application's workflow blocks
|
||||
* @param props - Component properties including icon, color, name, tags, and className
|
||||
* @returns A styled block card component
|
||||
*/
|
||||
@@ -39,33 +40,37 @@ export const LandingBlock = React.memo(function LandingBlock({
|
||||
tags,
|
||||
className,
|
||||
}: LandingBlockProps) {
|
||||
const hasContentBelowHeader = tags && tags.length > 0
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`z-10 flex w-64 flex-col items-start gap-3 rounded-[14px] border border-[#E5E5E5] bg-[#FEFEFE] p-3 ${className ?? ''}`}
|
||||
style={{
|
||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
}}
|
||||
className={`z-10 flex w-[250px] flex-col rounded-[8px] border border-[#E5E5E5] bg-white ${className ?? ''}`}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
{/* Header - matches workflow-block.tsx header styling */}
|
||||
<div
|
||||
className={`flex items-center justify-between p-[8px] ${hasContentBelowHeader ? 'border-[#E5E5E5] border-b' : ''}`}
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
|
||||
<div
|
||||
className='flex h-6 w-6 items-center justify-center rounded-[8px] text-white'
|
||||
style={{ backgroundColor: color as string }}
|
||||
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
||||
style={{ background: color as string }}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<p className='text-base text-card-foreground'>{name}</p>
|
||||
<span className='truncate font-medium text-[#171717] text-[16px]' title={name}>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
<BookIcon className='h-4 w-4 text-muted-foreground' />
|
||||
</div>
|
||||
|
||||
{tags && tags.length > 0 ? (
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{/* Content - SubBlock Rows matching workflow-block.tsx */}
|
||||
{hasContentBelowHeader && (
|
||||
<div className='flex flex-col gap-[8px] p-[8px]'>
|
||||
{tags.map((tag) => (
|
||||
<Tag key={tag.label} icon={tag.icon} label={tag.label} />
|
||||
<SubBlockRow key={tag.label} icon={tag.icon} label={tag.label} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -7,9 +7,14 @@ import {
|
||||
type LandingCardData,
|
||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
||||
|
||||
/**
|
||||
* Handle Y offset from block top - matches HANDLE_POSITIONS.DEFAULT_Y_OFFSET
|
||||
*/
|
||||
const HANDLE_Y_OFFSET = 20
|
||||
|
||||
/**
|
||||
* React Flow node component for the landing canvas
|
||||
* Includes CSS animations and connection handles
|
||||
* Styled to match the application's workflow blocks
|
||||
* @param props - Component properties containing node data
|
||||
* @returns A React Flow compatible node component
|
||||
*/
|
||||
@@ -41,15 +46,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#FEFEFE',
|
||||
border: '1px solid #E5E5E5',
|
||||
borderRadius: '50%',
|
||||
top: '50%',
|
||||
left: '-20px',
|
||||
width: '7px',
|
||||
height: '20px',
|
||||
background: '#D1D1D1',
|
||||
border: 'none',
|
||||
borderRadius: '2px 0 0 2px',
|
||||
top: `${HANDLE_Y_OFFSET}px`,
|
||||
left: '-7px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
zIndex: 10,
|
||||
}}
|
||||
isConnectable={false}
|
||||
/>
|
||||
@@ -59,15 +64,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#FEFEFE',
|
||||
border: '1px solid #E5E5E5',
|
||||
borderRadius: '50%',
|
||||
top: '50%',
|
||||
right: '-20px',
|
||||
width: '7px',
|
||||
height: '20px',
|
||||
background: '#D1D1D1',
|
||||
border: 'none',
|
||||
borderRadius: '0 2px 2px 0',
|
||||
top: `${HANDLE_Y_OFFSET}px`,
|
||||
right: '-7px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
zIndex: 10,
|
||||
}}
|
||||
isConnectable={false}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface LoopBlockProps {
|
||||
/**
|
||||
* Loop block container component that provides a styled container
|
||||
* for grouping related elements with a dashed border
|
||||
* Styled to match the application's subflow containers
|
||||
* @param props - Component properties including children and styling
|
||||
* @returns A styled loop container component
|
||||
*/
|
||||
@@ -29,33 +30,33 @@ export const LoopBlock = React.memo(function LoopBlock({
|
||||
style={{
|
||||
width: '1198px',
|
||||
height: '528px',
|
||||
borderRadius: '14px',
|
||||
background: 'rgba(59, 130, 246, 0.10)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
position: 'relative',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{/* Custom dashed border with SVG */}
|
||||
{/* Custom dashed border with SVG - 8px border radius to match blocks */}
|
||||
<svg
|
||||
className='pointer-events-none absolute inset-0 h-full w-full'
|
||||
style={{ borderRadius: '14px' }}
|
||||
style={{ borderRadius: '8px' }}
|
||||
preserveAspectRatio='none'
|
||||
>
|
||||
<path
|
||||
className='landing-loop-animated-dash'
|
||||
d='M 1183.5 527.5
|
||||
L 14 527.5
|
||||
A 13.5 13.5 0 0 1 0.5 514
|
||||
L 0.5 14
|
||||
A 13.5 13.5 0 0 1 14 0.5
|
||||
L 1183.5 0.5
|
||||
A 13.5 13.5 0 0 1 1197 14
|
||||
L 1197 514
|
||||
A 13.5 13.5 0 0 1 1183.5 527.5 Z'
|
||||
d='M 1190 527.5
|
||||
L 8 527.5
|
||||
A 7.5 7.5 0 0 1 0.5 520
|
||||
L 0.5 8
|
||||
A 7.5 7.5 0 0 1 8 0.5
|
||||
L 1190 0.5
|
||||
A 7.5 7.5 0 0 1 1197.5 8
|
||||
L 1197.5 520
|
||||
A 7.5 7.5 0 0 1 1190 527.5 Z'
|
||||
fill='none'
|
||||
stroke='#3B82F6'
|
||||
strokeWidth='1'
|
||||
strokeDasharray='12 12'
|
||||
strokeDasharray='8 8'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -1,25 +1,52 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Properties for a tag component
|
||||
* Properties for a subblock row component
|
||||
* Matches the SubBlockRow pattern from workflow-block.tsx
|
||||
*/
|
||||
export interface TagProps {
|
||||
/** Icon element to display in the tag */
|
||||
icon: React.ReactNode
|
||||
/** Text label for the tag */
|
||||
export interface SubBlockRowProps {
|
||||
/** Icon element to display (optional, for visual context) */
|
||||
icon?: React.ReactNode
|
||||
/** Text label for the row title */
|
||||
label: string
|
||||
/** Optional value to display on the right side */
|
||||
value?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag component for displaying labeled icons in a compact format
|
||||
* @param props - Tag properties including icon and label
|
||||
* @returns A styled tag component
|
||||
* Kept for backwards compatibility
|
||||
*/
|
||||
export const Tag = React.memo(function Tag({ icon, label }: TagProps) {
|
||||
export type TagProps = SubBlockRowProps
|
||||
|
||||
/**
|
||||
* SubBlockRow component matching the workflow block's subblock row style
|
||||
* @param props - Row properties including label and optional value
|
||||
* @returns A styled row component
|
||||
*/
|
||||
export const SubBlockRow = React.memo(function SubBlockRow({ label, value }: SubBlockRowProps) {
|
||||
// Split label by colon to separate title and value if present
|
||||
const [title, displayValue] = label.includes(':')
|
||||
? label.split(':').map((s) => s.trim())
|
||||
: [label, value]
|
||||
|
||||
return (
|
||||
<div className='flex w-fit items-center gap-1 rounded-[8px] border border-gray-300 bg-white px-2 py-0.5'>
|
||||
<div className='h-3 w-3 text-muted-foreground'>{icon}</div>
|
||||
<p className='text-muted-foreground text-xs leading-normal'>{label}</p>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<span className='min-w-0 truncate text-[#888888] text-[14px] capitalize' title={title}>
|
||||
{title}
|
||||
</span>
|
||||
{displayValue && (
|
||||
<span
|
||||
className='flex-1 truncate text-right text-[#171717] text-[14px]'
|
||||
title={displayValue}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tag component - alias for SubBlockRow for backwards compatibility
|
||||
*/
|
||||
export const Tag = SubBlockRow
|
||||
|
||||
@@ -9,9 +9,10 @@ import { LandingFlow } from '@/app/(landing)/components/hero/components/landing-
|
||||
|
||||
/**
|
||||
* Visual constants for landing node dimensions
|
||||
* Matches BLOCK_DIMENSIONS from the application
|
||||
*/
|
||||
export const CARD_WIDTH = 256
|
||||
export const CARD_HEIGHT = 92
|
||||
export const CARD_WIDTH = 250
|
||||
export const CARD_HEIGHT = 100
|
||||
|
||||
/**
|
||||
* Landing block node with positioning information
|
||||
|
||||
@@ -4,33 +4,29 @@ import React from 'react'
|
||||
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
||||
|
||||
/**
|
||||
* Custom edge component with animated dotted line that floats between handles
|
||||
* Custom edge component with animated dashed line
|
||||
* Styled to match the application's workflow edges with rectangular handles
|
||||
* @param props - React Flow edge properties
|
||||
* @returns An animated dotted edge component
|
||||
* @returns An animated dashed edge component
|
||||
*/
|
||||
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
||||
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, data } =
|
||||
props
|
||||
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style } = props
|
||||
|
||||
// Adjust the connection points to create floating effect
|
||||
// Account for handle size (12px) and additional spacing
|
||||
const handleRadius = 6 // Half of handle width (12px)
|
||||
const floatingGap = 1 // Additional gap for floating effect
|
||||
|
||||
// Calculate adjusted positions based on edge direction
|
||||
// Adjust the connection points to connect flush with rectangular handles
|
||||
// Handle width is 7px, positioned at -7px from edge
|
||||
let adjustedSourceX = sourceX
|
||||
let adjustedTargetX = targetX
|
||||
|
||||
if (sourcePosition === Position.Right) {
|
||||
adjustedSourceX = sourceX + handleRadius + floatingGap
|
||||
adjustedSourceX = sourceX + 1
|
||||
} else if (sourcePosition === Position.Left) {
|
||||
adjustedSourceX = sourceX - handleRadius - floatingGap
|
||||
adjustedSourceX = sourceX - 1
|
||||
}
|
||||
|
||||
if (targetPosition === Position.Left) {
|
||||
adjustedTargetX = targetX - handleRadius - floatingGap
|
||||
adjustedTargetX = targetX - 1
|
||||
} else if (targetPosition === Position.Right) {
|
||||
adjustedTargetX = targetX + handleRadius + floatingGap
|
||||
adjustedTargetX = targetX + 1
|
||||
}
|
||||
|
||||
const [path] = getSmoothStepPath({
|
||||
@@ -40,8 +36,8 @@ export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: 20,
|
||||
offset: 10,
|
||||
borderRadius: 8,
|
||||
offset: 16,
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import {
|
||||
ArrowUp,
|
||||
BinaryIcon,
|
||||
BookIcon,
|
||||
CalendarIcon,
|
||||
CodeIcon,
|
||||
Globe2Icon,
|
||||
MessageSquareIcon,
|
||||
VariableIcon,
|
||||
} from 'lucide-react'
|
||||
import { ArrowUp, CodeIcon } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { type Edge, type Node, Position } from 'reactflow'
|
||||
import {
|
||||
@@ -23,7 +14,6 @@ import {
|
||||
JiraIcon,
|
||||
LinearIcon,
|
||||
NotionIcon,
|
||||
OpenAIIcon,
|
||||
OutlookIcon,
|
||||
PackageSearchIcon,
|
||||
PineconeIcon,
|
||||
@@ -65,67 +55,56 @@ const SERVICE_TEMPLATES = {
|
||||
|
||||
/**
|
||||
* Landing blocks for the canvas preview
|
||||
* Styled to match the application's workflow blocks with subblock rows
|
||||
*/
|
||||
const LANDING_BLOCKS: LandingManualBlock[] = [
|
||||
{
|
||||
id: 'schedule',
|
||||
name: 'Schedule',
|
||||
color: '#7B68EE',
|
||||
icon: <ScheduleIcon className='h-4 w-4' />,
|
||||
icon: <ScheduleIcon className='h-[16px] w-[16px] text-white' />,
|
||||
positions: {
|
||||
mobile: { x: 8, y: 60 },
|
||||
tablet: { x: 40, y: 120 },
|
||||
desktop: { x: 60, y: 180 },
|
||||
},
|
||||
tags: [
|
||||
{ icon: <CalendarIcon className='h-3 w-3' />, label: '09:00AM Daily' },
|
||||
{ icon: <Globe2Icon className='h-3 w-3' />, label: 'PST' },
|
||||
],
|
||||
tags: [{ label: 'Time: 09:00AM Daily' }, { label: 'Timezone: PST' }],
|
||||
},
|
||||
{
|
||||
id: 'knowledge',
|
||||
name: 'Knowledge',
|
||||
color: '#00B0B0',
|
||||
icon: <PackageSearchIcon className='h-4 w-4' />,
|
||||
icon: <PackageSearchIcon className='h-[16px] w-[16px] text-white' />,
|
||||
positions: {
|
||||
mobile: { x: 120, y: 140 },
|
||||
tablet: { x: 220, y: 200 },
|
||||
desktop: { x: 420, y: 241 },
|
||||
},
|
||||
tags: [
|
||||
{ icon: <BookIcon className='h-3 w-3' />, label: 'Product Vector DB' },
|
||||
{ icon: <BinaryIcon className='h-3 w-3' />, label: 'Limit: 10' },
|
||||
],
|
||||
tags: [{ label: 'Source: Product Vector DB' }, { label: 'Limit: 10' }],
|
||||
},
|
||||
{
|
||||
id: 'agent',
|
||||
name: 'Agent',
|
||||
color: '#802FFF',
|
||||
icon: <AgentIcon className='h-4 w-4' />,
|
||||
icon: <AgentIcon className='h-[16px] w-[16px] text-white' />,
|
||||
positions: {
|
||||
mobile: { x: 340, y: 60 },
|
||||
tablet: { x: 540, y: 120 },
|
||||
desktop: { x: 880, y: 142 },
|
||||
},
|
||||
tags: [
|
||||
{ icon: <OpenAIIcon className='h-3 w-3' />, label: 'gpt-5' },
|
||||
{ icon: <MessageSquareIcon className='h-3 w-3' />, label: 'You are a support ag...' },
|
||||
],
|
||||
tags: [{ label: 'Model: gpt-5' }, { label: 'Prompt: You are a support ag...' }],
|
||||
},
|
||||
{
|
||||
id: 'function',
|
||||
name: 'Function',
|
||||
color: '#FF402F',
|
||||
icon: <CodeIcon className='h-4 w-4' />,
|
||||
icon: <CodeIcon className='h-[16px] w-[16px] text-white' />,
|
||||
positions: {
|
||||
mobile: { x: 480, y: 220 },
|
||||
tablet: { x: 740, y: 280 },
|
||||
desktop: { x: 880, y: 340 },
|
||||
},
|
||||
tags: [
|
||||
{ icon: <CodeIcon className='h-3 w-3' />, label: 'Python' },
|
||||
{ icon: <VariableIcon className='h-3 w-3' />, label: 'time = "2025-09-01...' },
|
||||
],
|
||||
tags: [{ label: 'Language: Python' }, { label: 'Code: time = "2025-09-01...' }],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ function PricingCard({
|
||||
*/
|
||||
export default function LandingPricing() {
|
||||
return (
|
||||
<section id='pricing' className='px-4 pt-[19px] sm:px-0 sm:pt-0' aria-label='Pricing plans'>
|
||||
<section id='pricing' className='px-4 pt-[23px] sm:px-0 sm:pt-[4px]' aria-label='Pricing plans'>
|
||||
<h2 className='sr-only'>Pricing Plans</h2>
|
||||
<div className='relative mx-auto w-full max-w-[1289px]'>
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
||||
|
||||
@@ -21,7 +21,7 @@ interface NavProps {
|
||||
}
|
||||
|
||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||
const [githubStars, setGithubStars] = useState('25.8k')
|
||||
const [githubStars, setGithubStars] = useState('26.1k')
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
27
apps/sim/app/(landing)/studio/[slug]/back-link.tsx
Normal file
27
apps/sim/app/(landing)/studio/[slug]/back-link.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, ChevronLeft } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export function BackLink() {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
return (
|
||||
<Link
|
||||
href='/studio'
|
||||
className='group flex items-center gap-1 text-gray-600 text-sm hover:text-gray-900'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<span className='group-hover:-translate-x-0.5 inline-flex transition-transform duration-200'>
|
||||
{isHovered ? (
|
||||
<ArrowLeft className='h-4 w-4' aria-hidden='true' />
|
||||
) : (
|
||||
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
|
||||
)}
|
||||
</span>
|
||||
Back to Sim Studio
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
||||
import { FAQ } from '@/lib/blog/faq'
|
||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
||||
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { BackLink } from '@/app/(landing)/studio/[slug]/back-link'
|
||||
import { ShareButton } from '@/app/(landing)/studio/[slug]/share-button'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getAllPostMeta()
|
||||
@@ -48,9 +51,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
/>
|
||||
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
||||
<div className='mb-6'>
|
||||
<Link href='/studio' className='text-gray-600 text-sm hover:text-gray-900'>
|
||||
← Back to Sim Studio
|
||||
</Link>
|
||||
<BackLink />
|
||||
</div>
|
||||
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
||||
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
||||
@@ -75,28 +76,31 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
<div className='mt-4 flex items-center gap-3'>
|
||||
{(post.authors || [post.author]).map((a, idx) => (
|
||||
<div key={idx} className='flex items-center gap-2'>
|
||||
{a?.avatarUrl ? (
|
||||
<Avatar className='size-6'>
|
||||
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
||||
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
) : null}
|
||||
<Link
|
||||
href={a?.url || '#'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer author'
|
||||
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
|
||||
itemProp='author'
|
||||
itemScope
|
||||
itemType='https://schema.org/Person'
|
||||
>
|
||||
<span itemProp='name'>{a?.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<div className='mt-4 flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
{(post.authors || [post.author]).map((a, idx) => (
|
||||
<div key={idx} className='flex items-center gap-2'>
|
||||
{a?.avatarUrl ? (
|
||||
<Avatar className='size-6'>
|
||||
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
||||
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
) : null}
|
||||
<Link
|
||||
href={a?.url || '#'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer author'
|
||||
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
|
||||
itemProp='author'
|
||||
itemScope
|
||||
itemType='https://schema.org/Person'
|
||||
>
|
||||
<span itemProp='name'>{a?.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ShareButton url={`${getBaseUrl()}/studio/${slug}`} title={post.title} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
65
apps/sim/app/(landing)/studio/[slug]/share-button.tsx
Normal file
65
apps/sim/app/(landing)/studio/[slug]/share-button.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Share2 } from 'lucide-react'
|
||||
import { Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
|
||||
|
||||
interface ShareButtonProps {
|
||||
url: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export function ShareButton({ url, title }: ShareButtonProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setCopied(true)
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
setOpen(false)
|
||||
}, 1000)
|
||||
} catch {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShareTwitter = () => {
|
||||
const tweetUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`
|
||||
window.open(tweetUrl, '_blank', 'noopener,noreferrer')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleShareLinkedIn = () => {
|
||||
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`
|
||||
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className='flex items-center gap-1.5 text-gray-600 text-sm hover:text-gray-900'
|
||||
aria-label='Share this post'
|
||||
>
|
||||
<Share2 className='h-4 w-4' />
|
||||
<span>Share</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align='end' minWidth={140}>
|
||||
<PopoverItem onClick={handleCopyLink}>{copied ? 'Copied!' : 'Copy link'}</PopoverItem>
|
||||
<PopoverItem onClick={handleShareTwitter}>Share on X</PopoverItem>
|
||||
<PopoverItem onClick={handleShareLinkedIn}>Share on LinkedIn</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export default async function StudioIndex({
|
||||
? filtered.sort((a, b) => {
|
||||
if (a.featured && !b.featured) return -1
|
||||
if (!a.featured && b.featured) return 1
|
||||
return 0
|
||||
return new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
})
|
||||
: filtered
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getRedisClient } from '@/lib/core/config/redis'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('A2AAgentCardAPI')
|
||||
|
||||
@@ -95,6 +96,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<Ro
|
||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
|
||||
if (!workspaceAccess.canWrite) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
if (
|
||||
@@ -160,6 +166,11 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
|
||||
if (!workspaceAccess.canWrite) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
|
||||
|
||||
logger.info(`Deleted A2A agent: ${agentId}`)
|
||||
@@ -194,6 +205,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
|
||||
if (!workspaceAccess.canWrite) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const action = body.action as 'publish' | 'unpublish' | 'refresh'
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
|
||||
import { validateExternalUrl } from '@/lib/core/security/input-validation'
|
||||
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||
@@ -1118,17 +1119,13 @@ async function handlePushNotificationSet(
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(params.pushNotificationConfig.url)
|
||||
if (url.protocol !== 'https:') {
|
||||
return NextResponse.json(
|
||||
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Push notification URL must use HTTPS'),
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
const urlValidation = validateExternalUrl(
|
||||
params.pushNotificationConfig.url,
|
||||
'Push notification URL'
|
||||
)
|
||||
if (!urlValidation.isValid) {
|
||||
return NextResponse.json(
|
||||
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification URL'),
|
||||
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, urlValidation.error || 'Invalid URL'),
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,17 +104,11 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
|
||||
// Build execution params starting with LLM-provided arguments
|
||||
// Resolve all {{ENV_VAR}} references in the arguments
|
||||
// Resolve all {{ENV_VAR}} references in the arguments (deep for nested objects)
|
||||
const executionParams: Record<string, any> = resolveEnvVarReferences(
|
||||
toolArgs,
|
||||
decryptedEnvVars,
|
||||
{
|
||||
resolveExactMatch: true,
|
||||
allowEmbedded: true,
|
||||
trimKeys: true,
|
||||
onMissing: 'keep',
|
||||
deep: true,
|
||||
}
|
||||
{ deep: true }
|
||||
) as Record<string, any>
|
||||
|
||||
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {
|
||||
|
||||
@@ -84,6 +84,14 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
checkInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
authType: 'internal_jwt',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/execution/e2b', () => ({
|
||||
executeInE2B: vi.fn(),
|
||||
}))
|
||||
@@ -110,6 +118,24 @@ describe('Function Execute API Route', () => {
|
||||
})
|
||||
|
||||
describe('Security Tests', () => {
|
||||
it('should reject unauthorized requests', async () => {
|
||||
const { checkInternalAuth } = await import('@/lib/auth/hybrid')
|
||||
vi.mocked(checkInternalAuth).mockResolvedValueOnce({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test"',
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toHaveProperty('error', 'Unauthorized')
|
||||
})
|
||||
|
||||
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test"',
|
||||
@@ -313,7 +339,7 @@ describe('Function Execute API Route', () => {
|
||||
'block-2': 'world',
|
||||
},
|
||||
blockNameMapping: {
|
||||
validVar: 'block-1',
|
||||
validvar: 'block-1',
|
||||
another_valid: 'block-2',
|
||||
},
|
||||
})
|
||||
@@ -539,7 +565,7 @@ describe('Function Execute API Route', () => {
|
||||
'block-complex': complexData,
|
||||
},
|
||||
blockNameMapping: {
|
||||
complexData: 'block-complex',
|
||||
complexdata: 'block-complex',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { executeInE2B } from '@/lib/execution/e2b'
|
||||
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
|
||||
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
||||
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
|
||||
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
|
||||
import {
|
||||
createEnvVarPattern,
|
||||
createWorkflowVariablePattern,
|
||||
} from '@/executor/utils/reference-validation'
|
||||
import { navigatePath } from '@/executor/variables/resolvers/reference'
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
@@ -470,14 +471,17 @@ function resolveEnvironmentVariables(
|
||||
|
||||
function resolveTagVariables(
|
||||
code: string,
|
||||
blockData: Record<string, any>,
|
||||
blockData: Record<string, unknown>,
|
||||
blockNameMapping: Record<string, string>,
|
||||
contextVariables: Record<string, any>
|
||||
blockOutputSchemas: Record<string, OutputSchema>,
|
||||
contextVariables: Record<string, unknown>,
|
||||
language = 'javascript'
|
||||
): string {
|
||||
let resolvedCode = code
|
||||
const undefinedLiteral = language === 'python' ? 'None' : 'undefined'
|
||||
|
||||
const tagPattern = new RegExp(
|
||||
`${REFERENCE.START}([a-zA-Z_][a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])${REFERENCE.END}`,
|
||||
`${REFERENCE.START}([a-zA-Z_](?:[a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])?)${REFERENCE.END}`,
|
||||
'g'
|
||||
)
|
||||
const tagMatches = resolvedCode.match(tagPattern) || []
|
||||
@@ -486,41 +490,37 @@ function resolveTagVariables(
|
||||
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
|
||||
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
|
||||
const blockName = pathParts[0]
|
||||
const fieldPath = pathParts.slice(1)
|
||||
|
||||
const blockId = blockNameMapping[blockName]
|
||||
if (!blockId) {
|
||||
const result = resolveBlockReference(blockName, fieldPath, {
|
||||
blockNameMapping,
|
||||
blockData,
|
||||
blockOutputSchemas,
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
continue
|
||||
}
|
||||
|
||||
const blockOutput = blockData[blockId]
|
||||
if (blockOutput === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
let tagValue: any
|
||||
if (pathParts.length === 1) {
|
||||
tagValue = blockOutput
|
||||
} else {
|
||||
tagValue = navigatePath(blockOutput, pathParts.slice(1))
|
||||
}
|
||||
let tagValue = result.value
|
||||
|
||||
if (tagValue === undefined) {
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral)
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
typeof tagValue === 'string' &&
|
||||
tagValue.length > 100 &&
|
||||
(tagValue.startsWith('{') || tagValue.startsWith('['))
|
||||
) {
|
||||
try {
|
||||
tagValue = JSON.parse(tagValue)
|
||||
} catch {
|
||||
// Keep as-is
|
||||
if (typeof tagValue === 'string') {
|
||||
const trimmed = tagValue.trimStart()
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try {
|
||||
tagValue = JSON.parse(tagValue)
|
||||
} catch {
|
||||
// Keep as string if not valid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||
const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}`
|
||||
contextVariables[safeVarName] = tagValue
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
}
|
||||
@@ -537,18 +537,27 @@ function resolveTagVariables(
|
||||
*/
|
||||
function resolveCodeVariables(
|
||||
code: string,
|
||||
params: Record<string, any>,
|
||||
params: Record<string, unknown>,
|
||||
envVars: Record<string, string> = {},
|
||||
blockData: Record<string, any> = {},
|
||||
blockData: Record<string, unknown> = {},
|
||||
blockNameMapping: Record<string, string> = {},
|
||||
workflowVariables: Record<string, any> = {}
|
||||
): { resolvedCode: string; contextVariables: Record<string, any> } {
|
||||
blockOutputSchemas: Record<string, OutputSchema> = {},
|
||||
workflowVariables: Record<string, unknown> = {},
|
||||
language = 'javascript'
|
||||
): { resolvedCode: string; contextVariables: Record<string, unknown> } {
|
||||
let resolvedCode = code
|
||||
const contextVariables: Record<string, any> = {}
|
||||
const contextVariables: Record<string, unknown> = {}
|
||||
|
||||
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
|
||||
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
|
||||
resolvedCode = resolveTagVariables(resolvedCode, blockData, blockNameMapping, contextVariables)
|
||||
resolvedCode = resolveTagVariables(
|
||||
resolvedCode,
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
blockOutputSchemas,
|
||||
contextVariables,
|
||||
language
|
||||
)
|
||||
|
||||
return { resolvedCode, contextVariables }
|
||||
}
|
||||
@@ -573,6 +582,12 @@ export async function POST(req: NextRequest) {
|
||||
let resolvedCode = '' // Store resolved code for error reporting
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(req)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized function execution attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
|
||||
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
|
||||
@@ -585,6 +600,7 @@ export async function POST(req: NextRequest) {
|
||||
envVars = {},
|
||||
blockData = {},
|
||||
blockNameMapping = {},
|
||||
blockOutputSchemas = {},
|
||||
workflowVariables = {},
|
||||
workflowId,
|
||||
isCustomTool = false,
|
||||
@@ -601,20 +617,21 @@ export async function POST(req: NextRequest) {
|
||||
isCustomTool,
|
||||
})
|
||||
|
||||
// Resolve variables in the code with workflow environment variables
|
||||
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
|
||||
|
||||
const codeResolution = resolveCodeVariables(
|
||||
code,
|
||||
executionParams,
|
||||
envVars,
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
workflowVariables
|
||||
blockOutputSchemas,
|
||||
workflowVariables,
|
||||
lang
|
||||
)
|
||||
resolvedCode = codeResolution.resolvedCode
|
||||
const contextVariables = codeResolution.contextVariables
|
||||
|
||||
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
|
||||
|
||||
let jsImports = ''
|
||||
let jsRemainingCode = resolvedCode
|
||||
let hasImports = false
|
||||
@@ -670,7 +687,11 @@ export async function POST(req: NextRequest) {
|
||||
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
|
||||
prologueLineCount++
|
||||
for (const [k, v] of Object.entries(contextVariables)) {
|
||||
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
|
||||
if (v === undefined) {
|
||||
prologue += `const ${k} = undefined;\n`
|
||||
} else {
|
||||
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
|
||||
}
|
||||
prologueLineCount++
|
||||
}
|
||||
|
||||
@@ -741,7 +762,11 @@ export async function POST(req: NextRequest) {
|
||||
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
|
||||
prologueLineCount++
|
||||
for (const [k, v] of Object.entries(contextVariables)) {
|
||||
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
|
||||
if (v === undefined) {
|
||||
prologue += `${k} = None\n`
|
||||
} else {
|
||||
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
|
||||
}
|
||||
prologueLineCount++
|
||||
}
|
||||
const wrapped = [
|
||||
|
||||
@@ -157,7 +157,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||
'kb-123',
|
||||
{
|
||||
includeDisabled: false,
|
||||
enabledFilter: undefined,
|
||||
search: undefined,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
@@ -166,7 +166,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should filter disabled documents by default', async () => {
|
||||
it('should return documents with default filter', async () => {
|
||||
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
||||
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
||||
|
||||
@@ -194,7 +194,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||
'kb-123',
|
||||
{
|
||||
includeDisabled: false,
|
||||
enabledFilter: undefined,
|
||||
search: undefined,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
@@ -203,7 +203,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should include disabled documents when requested', async () => {
|
||||
it('should filter documents by enabled status when requested', async () => {
|
||||
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
||||
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?includeDisabled=true'
|
||||
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?enabledFilter=disabled'
|
||||
const req = new Request(url, { method: 'GET' }) as any
|
||||
|
||||
const { GET } = await import('@/app/api/knowledge/[id]/documents/route')
|
||||
@@ -233,7 +233,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||
'kb-123',
|
||||
{
|
||||
includeDisabled: true,
|
||||
enabledFilter: 'disabled',
|
||||
search: undefined,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
@@ -361,8 +361,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith(
|
||||
validDocumentData,
|
||||
'kb-123',
|
||||
expect.any(String),
|
||||
'user-123'
|
||||
expect.any(String)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -470,8 +469,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith(
|
||||
validBulkData.documents,
|
||||
'kb-123',
|
||||
expect.any(String),
|
||||
'user-123'
|
||||
expect.any(String)
|
||||
)
|
||||
expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import {
|
||||
bulkDocumentOperation,
|
||||
bulkDocumentOperationByFilter,
|
||||
createDocumentRecords,
|
||||
createSingleDocument,
|
||||
getDocuments,
|
||||
@@ -57,13 +58,20 @@ const BulkCreateDocumentsSchema = z.object({
|
||||
bulk: z.literal(true),
|
||||
})
|
||||
|
||||
const BulkUpdateDocumentsSchema = z.object({
|
||||
operation: z.enum(['enable', 'disable', 'delete']),
|
||||
documentIds: z
|
||||
.array(z.string())
|
||||
.min(1, 'At least one document ID is required')
|
||||
.max(100, 'Cannot operate on more than 100 documents at once'),
|
||||
})
|
||||
const BulkUpdateDocumentsSchema = z
|
||||
.object({
|
||||
operation: z.enum(['enable', 'disable', 'delete']),
|
||||
documentIds: z
|
||||
.array(z.string())
|
||||
.min(1, 'At least one document ID is required')
|
||||
.max(100, 'Cannot operate on more than 100 documents at once')
|
||||
.optional(),
|
||||
selectAll: z.boolean().optional(),
|
||||
enabledFilter: z.enum(['all', 'enabled', 'disabled']).optional(),
|
||||
})
|
||||
.refine((data) => data.selectAll || (data.documentIds && data.documentIds.length > 0), {
|
||||
message: 'Either selectAll must be true or documentIds must be provided',
|
||||
})
|
||||
|
||||
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
@@ -90,14 +98,17 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
}
|
||||
|
||||
const url = new URL(req.url)
|
||||
const includeDisabled = url.searchParams.get('includeDisabled') === 'true'
|
||||
const enabledFilter = url.searchParams.get('enabledFilter') as
|
||||
| 'all'
|
||||
| 'enabled'
|
||||
| 'disabled'
|
||||
| null
|
||||
const search = url.searchParams.get('search') || undefined
|
||||
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
|
||||
const offset = Number.parseInt(url.searchParams.get('offset') || '0')
|
||||
const sortByParam = url.searchParams.get('sortBy')
|
||||
const sortOrderParam = url.searchParams.get('sortOrder')
|
||||
|
||||
// Validate sort parameters
|
||||
const validSortFields: DocumentSortField[] = [
|
||||
'filename',
|
||||
'fileSize',
|
||||
@@ -105,6 +116,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
'chunkCount',
|
||||
'uploadedAt',
|
||||
'processingStatus',
|
||||
'enabled',
|
||||
]
|
||||
const validSortOrders: SortOrder[] = ['asc', 'desc']
|
||||
|
||||
@@ -120,7 +132,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
const result = await getDocuments(
|
||||
knowledgeBaseId,
|
||||
{
|
||||
includeDisabled,
|
||||
enabledFilter: enabledFilter || undefined,
|
||||
search,
|
||||
limit,
|
||||
offset,
|
||||
@@ -190,8 +202,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
const createdDocuments = await createDocumentRecords(
|
||||
validatedData.documents,
|
||||
knowledgeBaseId,
|
||||
requestId,
|
||||
userId
|
||||
requestId
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -250,16 +261,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
throw validationError
|
||||
}
|
||||
} else {
|
||||
// Handle single document creation
|
||||
try {
|
||||
const validatedData = CreateDocumentSchema.parse(body)
|
||||
|
||||
const newDocument = await createSingleDocument(
|
||||
validatedData,
|
||||
knowledgeBaseId,
|
||||
requestId,
|
||||
userId
|
||||
)
|
||||
const newDocument = await createSingleDocument(validatedData, knowledgeBaseId, requestId)
|
||||
|
||||
try {
|
||||
const { PlatformEvents } = await import('@/lib/core/telemetry')
|
||||
@@ -294,7 +299,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error creating document`, error)
|
||||
|
||||
// Check if it's a storage limit error
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to create document'
|
||||
const isStorageLimitError =
|
||||
errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit')
|
||||
@@ -331,16 +335,22 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
|
||||
|
||||
try {
|
||||
const validatedData = BulkUpdateDocumentsSchema.parse(body)
|
||||
const { operation, documentIds } = validatedData
|
||||
const { operation, documentIds, selectAll, enabledFilter } = validatedData
|
||||
|
||||
try {
|
||||
const result = await bulkDocumentOperation(
|
||||
knowledgeBaseId,
|
||||
operation,
|
||||
documentIds,
|
||||
requestId,
|
||||
session.user.id
|
||||
)
|
||||
let result
|
||||
if (selectAll) {
|
||||
result = await bulkDocumentOperationByFilter(
|
||||
knowledgeBaseId,
|
||||
operation,
|
||||
enabledFilter,
|
||||
requestId
|
||||
)
|
||||
} else if (documentIds && documentIds.length > 0) {
|
||||
result = await bulkDocumentOperation(knowledgeBaseId, operation, documentIds, requestId)
|
||||
} else {
|
||||
return NextResponse.json({ error: 'No documents specified' }, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||
import { McpClient } from '@/lib/mcp/client'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
|
||||
import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config'
|
||||
import type { McpTransport } from '@/lib/mcp/types'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
|
||||
|
||||
const logger = createLogger('McpServerTestAPI')
|
||||
|
||||
@@ -19,30 +18,6 @@ function isUrlBasedTransport(transport: McpTransport): boolean {
|
||||
return transport === 'streamable-http'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve environment variables in strings
|
||||
*/
|
||||
function resolveEnvVars(value: string, envVars: Record<string, string>): string {
|
||||
const missingVars: string[] = []
|
||||
const resolvedValue = resolveEnvVarReferences(value, envVars, {
|
||||
allowEmbedded: true,
|
||||
resolveExactMatch: true,
|
||||
trimKeys: true,
|
||||
onMissing: 'keep',
|
||||
deep: false,
|
||||
missingKeys: missingVars,
|
||||
}) as string
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
const uniqueMissing = Array.from(new Set(missingVars))
|
||||
uniqueMissing.forEach((envKey) => {
|
||||
logger.warn(`Environment variable "${envKey}" not found in MCP server test`)
|
||||
})
|
||||
}
|
||||
|
||||
return resolvedValue
|
||||
}
|
||||
|
||||
interface TestConnectionRequest {
|
||||
name: string
|
||||
transport: McpTransport
|
||||
@@ -96,39 +71,30 @@ export const POST = withMcpAuth('write')(
|
||||
)
|
||||
}
|
||||
|
||||
let resolvedUrl = body.url
|
||||
let resolvedHeaders = body.headers || {}
|
||||
|
||||
try {
|
||||
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
|
||||
|
||||
if (resolvedUrl) {
|
||||
resolvedUrl = resolveEnvVars(resolvedUrl, envVars)
|
||||
}
|
||||
|
||||
const resolvedHeadersObj: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(resolvedHeaders)) {
|
||||
resolvedHeadersObj[key] = resolveEnvVars(value, envVars)
|
||||
}
|
||||
resolvedHeaders = resolvedHeadersObj
|
||||
} catch (envError) {
|
||||
logger.warn(
|
||||
`[${requestId}] Failed to resolve environment variables, using raw values:`,
|
||||
envError
|
||||
)
|
||||
}
|
||||
|
||||
const testConfig: McpServerConfig = {
|
||||
// Build initial config for resolution
|
||||
const initialConfig = {
|
||||
id: `test-${requestId}`,
|
||||
name: body.name,
|
||||
transport: body.transport,
|
||||
url: resolvedUrl,
|
||||
headers: resolvedHeaders,
|
||||
url: body.url,
|
||||
headers: body.headers || {},
|
||||
timeout: body.timeout || 10000,
|
||||
retries: 1, // Only one retry for tests
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
// Resolve env vars using shared utility (non-strict mode for testing)
|
||||
const { config: testConfig, missingVars } = await resolveMcpConfigEnvVars(
|
||||
initialConfig,
|
||||
userId,
|
||||
workspaceId,
|
||||
{ strict: false }
|
||||
)
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
logger.warn(`[${requestId}] Some environment variables not found:`, { missingVars })
|
||||
}
|
||||
|
||||
const testSecurityPolicy = {
|
||||
requireConsent: false,
|
||||
auditLevel: 'none' as const,
|
||||
|
||||
@@ -3,7 +3,9 @@ import { account } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
import { executeProviderRequest } from '@/providers'
|
||||
@@ -20,6 +22,11 @@ export async function POST(request: NextRequest) {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Provider API request started`, {
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: request.headers.get('User-Agent'),
|
||||
@@ -85,6 +92,13 @@ export async function POST(request: NextRequest) {
|
||||
verbosity,
|
||||
})
|
||||
|
||||
if (workspaceId) {
|
||||
const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId)
|
||||
if (!workspaceAccess.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
let finalApiKey: string | undefined = apiKey
|
||||
try {
|
||||
if (provider === 'vertex' && vertexCredential) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createA2AClient } from '@/lib/a2a/utils'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { validateExternalUrl } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -39,6 +40,18 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const validatedData = A2ASetPushNotificationSchema.parse(body)
|
||||
|
||||
const urlValidation = validateExternalUrl(validatedData.webhookUrl, 'Webhook URL')
|
||||
if (!urlValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: urlValidation.error,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] A2A set push notification request`, {
|
||||
agentUrl: validatedData.agentUrl,
|
||||
taskId: validatedData.taskId,
|
||||
|
||||
@@ -181,7 +181,7 @@ describe('Custom Tools API Routes', () => {
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
authType: 'session',
|
||||
@@ -254,7 +254,7 @@ describe('Custom Tools API Routes', () => {
|
||||
)
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}),
|
||||
@@ -304,7 +304,7 @@ describe('Custom Tools API Routes', () => {
|
||||
describe('POST /api/tools/custom', () => {
|
||||
it('should reject unauthorized requests', async () => {
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}),
|
||||
@@ -390,7 +390,7 @@ describe('Custom Tools API Routes', () => {
|
||||
|
||||
it('should prevent unauthorized deletion of user-scoped tool', async () => {
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-456',
|
||||
authType: 'session',
|
||||
@@ -413,7 +413,7 @@ describe('Custom Tools API Routes', () => {
|
||||
|
||||
it('should reject unauthorized requests', async () => {
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq, isNull, or } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
@@ -42,8 +42,8 @@ export async function GET(request: NextRequest) {
|
||||
const workflowId = searchParams.get('workflowId')
|
||||
|
||||
try {
|
||||
// Use hybrid auth to support session, API key, and internal JWT
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
// Use session/internal auth to support session and internal JWT (no API key access)
|
||||
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized custom tools access attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
@@ -69,8 +69,8 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Check workspace permissions
|
||||
// For internal JWT with workflowId: checkHybridAuth already resolved userId from workflow owner
|
||||
// For session/API key: verify user has access to the workspace
|
||||
// For internal JWT with workflowId: checkSessionOrInternalAuth already resolved userId from workflow owner
|
||||
// For session: verify user has access to the workspace
|
||||
// For legacy (no workspaceId): skip workspace check, rely on userId match
|
||||
if (resolvedWorkspaceId && !(authResult.authType === 'internal_jwt' && workflowId)) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
@@ -116,8 +116,8 @@ export async function POST(req: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
// Use hybrid auth (though this endpoint is only called from UI)
|
||||
const authResult = await checkHybridAuth(req, { requireWorkflowId: false })
|
||||
// Use session/internal auth (no API key access)
|
||||
const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized custom tools update attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
@@ -193,8 +193,8 @@ export async function DELETE(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Use hybrid auth (though this endpoint is only called from UI)
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
// Use session/internal auth (no API key access)
|
||||
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized custom tool deletion attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateNumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail add label attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail archive attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail delete attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail mark read attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail mark unread attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail move attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail remove label attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail unarchive attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateImageUrl } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
|
||||
const imageUrl = url.searchParams.get('url')
|
||||
const requestId = generateRequestId()
|
||||
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
logger.error(`[${requestId}] Authentication failed for image proxy:`, authResult.error)
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { Resend } from 'resend'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized mail send attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Teams chat delete attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { StorageService } from '@/lib/uploads'
|
||||
@@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Mistral parse attempt`, {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||
|
||||
const logger = createLogger('MySQLDeleteAPI')
|
||||
@@ -21,6 +22,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MySQL delete attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = DeleteSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
||||
|
||||
const logger = createLogger('MySQLExecuteAPI')
|
||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MySQL execute attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = ExecuteSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||
|
||||
const logger = createLogger('MySQLInsertAPI')
|
||||
@@ -42,6 +43,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MySQL insert attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = InsertSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'
|
||||
|
||||
const logger = createLogger('MySQLIntrospectAPI')
|
||||
@@ -19,6 +20,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MySQL introspect attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = IntrospectSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
||||
|
||||
const logger = createLogger('MySQLQueryAPI')
|
||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MySQL query attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = QuerySchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||
|
||||
const logger = createLogger('MySQLUpdateAPI')
|
||||
@@ -40,6 +41,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MySQL update attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = UpdateSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import * as XLSX from 'xlsx'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
@@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook copy attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook delete attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook mark read attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook mark unread attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook move attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createPostgresConnection, executeDelete } from '@/app/api/tools/postgresql/utils'
|
||||
|
||||
const logger = createLogger('PostgreSQLDeleteAPI')
|
||||
@@ -21,6 +22,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized PostgreSQL delete attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = DeleteSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
createPostgresConnection,
|
||||
executeQuery,
|
||||
@@ -24,6 +25,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized PostgreSQL execute attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = ExecuteSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createPostgresConnection, executeInsert } from '@/app/api/tools/postgresql/utils'
|
||||
|
||||
const logger = createLogger('PostgreSQLInsertAPI')
|
||||
@@ -42,6 +43,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized PostgreSQL insert attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const params = InsertSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createPostgresConnection, executeIntrospect } from '@/app/api/tools/postgresql/utils'
|
||||
|
||||
const logger = createLogger('PostgreSQLIntrospectAPI')
|
||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized PostgreSQL introspect attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = IntrospectSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createPostgresConnection, executeQuery } from '@/app/api/tools/postgresql/utils'
|
||||
|
||||
const logger = createLogger('PostgreSQLQueryAPI')
|
||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized PostgreSQL query attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = QuerySchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createPostgresConnection, executeUpdate } from '@/app/api/tools/postgresql/utils'
|
||||
|
||||
const logger = createLogger('PostgreSQLUpdateAPI')
|
||||
@@ -40,6 +41,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized PostgreSQL update attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = UpdateSchema.parse(body)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { StorageService } from '@/lib/uploads'
|
||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Pulse parse attempt`, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { StorageService } from '@/lib/uploads'
|
||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Reducto parse attempt`, {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CopyObjectCommand, type ObjectCannedACL, S3Client } from '@aws-sdk/clie
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized S3 copy object attempt: ${authResult.error}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized S3 delete object attempt: ${authResult.error}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized S3 list objects attempt: ${authResult.error}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ObjectCannedACL, PutObjectCommand, S3Client } from '@aws-sdk/clien
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { SEARCH_TOOL_COST } from '@/lib/billing/constants'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
||||
const { searchParams: urlParams } = new URL(request.url)
|
||||
const workflowId = urlParams.get('workflowId') || undefined
|
||||
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
const errorMessage = workflowId ? 'Workflow not found' : authResult.error || 'Unauthorized'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import type { SFTPWrapper } from 'ssh2'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
createSftpConnection,
|
||||
@@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SFTP delete attempt: ${authResult.error}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from 'path'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils'
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SFTP download attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
createSftpConnection,
|
||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SFTP list attempt: ${authResult.error}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import type { SFTPWrapper } from 'ssh2'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
createSftpConnection,
|
||||
@@ -60,7 +60,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SFTP mkdir attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -44,7 +44,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SFTP upload attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -13,7 +13,7 @@ const SlackAddReactionSchema = z.object({
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -12,7 +12,7 @@ const SlackDeleteMessageSchema = z.object({
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { openDMChannel } from '../utils'
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Slack read messages attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { sendSlackMessage } from '../utils'
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Slack send attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Slack update message attempt: ${authResult.error}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { type SMSOptions, sendSMS } from '@/lib/messaging/sms/service'
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SMS send attempt: ${authResult.error}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createSSHConnection, escapeShellArg, executeSSHCommand } from '@/app/api/tools/ssh/utils'
|
||||
|
||||
const logger = createLogger('SSHCheckCommandExistsAPI')
|
||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SSH check command exists attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = CheckCommandExistsSchema.parse(body)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import type { Client, SFTPWrapper, Stats } from 'ssh2'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
createSSHConnection,
|
||||
getFileType,
|
||||
@@ -39,10 +40,15 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SSH check file exists attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = CheckFileExistsSchema.parse(body)
|
||||
|
||||
// Validate authentication
|
||||
if (!params.password && !params.privateKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either password or privateKey must be provided' },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
createSSHConnection,
|
||||
escapeShellArg,
|
||||
@@ -27,10 +28,15 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SSH create directory attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = CreateDirectorySchema.parse(body)
|
||||
|
||||
// Validate authentication
|
||||
if (!params.password && !params.privateKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either password or privateKey must be provided' },
|
||||
@@ -53,7 +59,6 @@ export async function POST(request: NextRequest) {
|
||||
const dirPath = sanitizePath(params.path)
|
||||
const escapedPath = escapeShellArg(dirPath)
|
||||
|
||||
// Check if directory already exists
|
||||
const checkResult = await executeSSHCommand(
|
||||
client,
|
||||
`test -d '${escapedPath}' && echo "exists"`
|
||||
@@ -70,7 +75,6 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
// Create directory
|
||||
const mkdirFlag = params.recursive ? '-p' : ''
|
||||
const command = `mkdir ${mkdirFlag} -m ${params.permissions} '${escapedPath}'`
|
||||
const result = await executeSSHCommand(client, command)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
createSSHConnection,
|
||||
escapeShellArg,
|
||||
@@ -27,10 +28,15 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SSH delete file attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = DeleteFileSchema.parse(body)
|
||||
|
||||
// Validate authentication
|
||||
if (!params.password && !params.privateKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either password or privateKey must be provided' },
|
||||
@@ -53,7 +59,6 @@ export async function POST(request: NextRequest) {
|
||||
const filePath = sanitizePath(params.path)
|
||||
const escapedPath = escapeShellArg(filePath)
|
||||
|
||||
// Check if path exists
|
||||
const checkResult = await executeSSHCommand(
|
||||
client,
|
||||
`test -e '${escapedPath}' && echo "exists"`
|
||||
@@ -62,7 +67,6 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: `Path does not exist: ${filePath}` }, { status: 404 })
|
||||
}
|
||||
|
||||
// Build delete command
|
||||
let command: string
|
||||
if (params.recursive) {
|
||||
command = params.force ? `rm -rf '${escapedPath}'` : `rm -r '${escapedPath}'`
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import type { Client, SFTPWrapper } from 'ssh2'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils'
|
||||
|
||||
const logger = createLogger('SSHDownloadFileAPI')
|
||||
@@ -34,10 +35,15 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SSH download file attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = DownloadFileSchema.parse(body)
|
||||
|
||||
// Validate authentication
|
||||
if (!params.password && !params.privateKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either password or privateKey must be provided' },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createSSHConnection, executeSSHCommand, sanitizeCommand } from '@/app/api/tools/ssh/utils'
|
||||
|
||||
const logger = createLogger('SSHExecuteCommandAPI')
|
||||
@@ -21,10 +22,15 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SSH execute command attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = ExecuteCommandSchema.parse(body)
|
||||
|
||||
// Validate authentication
|
||||
if (!params.password && !params.privateKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either password or privateKey must be provided' },
|
||||
@@ -44,7 +50,6 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
try {
|
||||
// Build command with optional working directory
|
||||
let command = sanitizeCommand(params.command)
|
||||
if (params.workingDirectory) {
|
||||
command = `cd "${params.workingDirectory}" && ${command}`
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createSSHConnection, escapeShellArg, executeSSHCommand } from '@/app/api/tools/ssh/utils'
|
||||
|
||||
const logger = createLogger('SSHExecuteScriptAPI')
|
||||
@@ -22,10 +23,15 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SSH execute script attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = ExecuteScriptSchema.parse(body)
|
||||
|
||||
// Validate authentication
|
||||
if (!params.password && !params.privateKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either password or privateKey must be provided' },
|
||||
@@ -45,13 +51,10 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
try {
|
||||
// Create a temporary script file, execute it, and clean up
|
||||
const scriptPath = `/tmp/sim_script_${requestId}.sh`
|
||||
const escapedScriptPath = escapeShellArg(scriptPath)
|
||||
const escapedInterpreter = escapeShellArg(params.interpreter)
|
||||
|
||||
// Build the command to create, execute, and clean up the script
|
||||
// Note: heredoc with quoted delimiter ('SIMEOF') prevents variable expansion
|
||||
let command = `cat > '${escapedScriptPath}' << 'SIMEOF'
|
||||
${params.script}
|
||||
SIMEOF
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user