mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-26 15:28:03 -05:00
Compare commits
33 Commits
fix/multi-
...
v0.5.66
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45371e521e | ||
|
|
0ce0f98aa5 | ||
|
|
dff1c9d083 | ||
|
|
b09f683072 | ||
|
|
a8bb0db660 | ||
|
|
af82820a28 | ||
|
|
4372841797 | ||
|
|
5e8c843241 | ||
|
|
7bf3d73ee6 | ||
|
|
7ffc11a738 | ||
|
|
be578e2ed7 | ||
|
|
f415e5edc4 | ||
|
|
13a6e6c3fa | ||
|
|
f5ab7f21ae | ||
|
|
bfb6fffe38 | ||
|
|
4fbec0a43f | ||
|
|
585f5e365b | ||
|
|
3792bdd252 | ||
|
|
eb5d1f3e5b | ||
|
|
54ab82c8dd | ||
|
|
f895bf469b | ||
|
|
dd3209af06 | ||
|
|
b6ba3b50a7 | ||
|
|
b304233062 | ||
|
|
57e4b49bd6 | ||
|
|
e12dd204ed | ||
|
|
3d9d9cbc54 | ||
|
|
0f4ec962ad | ||
|
|
4827866f9a | ||
|
|
3e697d9ed9 | ||
|
|
4431a1a484 | ||
|
|
4d1a9a3f22 | ||
|
|
eb07a080fb |
@@ -44,7 +44,7 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 1G
|
memory: 4G
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
|
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
|
||||||
|
|||||||
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -27,11 +27,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Extract version from commit message
|
- name: Extract version from commit message
|
||||||
id: extract
|
id: extract
|
||||||
env:
|
|
||||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
|
||||||
run: |
|
run: |
|
||||||
|
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
||||||
# Only tag versions on main branch
|
# 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]}"
|
VERSION="${BASH_REMATCH[1]}"
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
echo "is_release=true" >> $GITHUB_OUTPUT
|
echo "is_release=true" >> $GITHUB_OUTPUT
|
||||||
|
|||||||
@@ -119,19 +119,6 @@ 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 */
|
/* Desktop only: Apply custom navbar offset, sidebar width and margin offsets */
|
||||||
/* On mobile, let fumadocs handle the layout natively */
|
/* On mobile, let fumadocs handle the layout natively */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ Stellen Sie Sim auf Ihrer eigenen Infrastruktur mit Docker oder Kubernetes berei
|
|||||||
|
|
||||||
## Anforderungen
|
## Anforderungen
|
||||||
|
|
||||||
| Ressource | Klein | Standard | Produktion |
|
| Ressource | Minimum | Empfohlen |
|
||||||
|----------|-------|----------|------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 Kerne | 4 Kerne | 8+ Kerne |
|
| CPU | 2 Kerne | 4+ Kerne |
|
||||||
| RAM | 12 GB | 16 GB | 32+ GB |
|
| RAM | 12 GB | 16+ GB |
|
||||||
| Speicher | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| Speicher | 20 GB SSD | 50+ GB SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | Neueste Version |
|
| Docker | 20.10+ | Neueste Version |
|
||||||
|
|
||||||
**Klein**: Entwicklung, Tests, Einzelnutzer (1-5 Nutzer)
|
|
||||||
**Standard**: Teams (5-50 Nutzer), moderate Arbeitslasten
|
|
||||||
**Produktion**: Große Teams (50+ Nutzer), Hochverfügbarkeit, intensive Workflow-Ausführung
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Die Ressourcenanforderungen werden durch Workflow-Ausführung (isolated-vm Sandboxing), Dateiverarbeitung (In-Memory-Dokumentenparsing) und Vektoroperationen (pgvector) bestimmt. Arbeitsspeicher ist typischerweise der limitierende Faktor, nicht CPU. Produktionsdaten zeigen, dass die Hauptanwendung durchschnittlich 4-8 GB und bei hoher Last bis zu 12 GB benötigt.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
|
|||||||
@@ -124,44 +124,11 @@ Choose between four types of loops:
|
|||||||
3. Drag other blocks inside the loop container
|
3. Drag other blocks inside the loop container
|
||||||
4. Connect the blocks as needed
|
4. Connect the blocks as needed
|
||||||
|
|
||||||
### Referencing Loop Data
|
### Accessing Results
|
||||||
|
|
||||||
There's an important distinction between referencing loop data from **inside** vs **outside** the loop:
|
After a loop completes, you can access aggregated results:
|
||||||
|
|
||||||
<Tabs items={['Inside the Loop', 'Outside the Loop']}>
|
- **`<loop.results>`**: Array of results from all loop iterations
|
||||||
<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
|
## Example Use Cases
|
||||||
|
|
||||||
@@ -217,29 +184,28 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
|
|||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
Available **inside** the loop only:
|
|
||||||
<ul className="list-disc space-y-2 pl-6">
|
<ul className="list-disc space-y-2 pl-6">
|
||||||
<li>
|
<li>
|
||||||
<strong>{"<loop.index>"}</strong>: Current iteration number (0-based)
|
<strong>loop.currentItem</strong>: Current item being processed
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>{"<loop.currentItem>"}</strong>: Current item being processed (forEach only)
|
<strong>loop.index</strong>: Current iteration number (0-based)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>{"<loop.items>"}</strong>: Full collection (forEach only)
|
<strong>loop.items</strong>: Full collection (forEach loops)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
<ul className="list-disc space-y-2 pl-6">
|
<ul className="list-disc space-y-2 pl-6">
|
||||||
<li>
|
<li>
|
||||||
<strong>{"<blockname.results>"}</strong>: Array of all iteration results (accessed via block name)
|
<strong>loop.results</strong>: Array of all iteration results
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Structure</strong>: Results maintain iteration order
|
<strong>Structure</strong>: Results maintain iteration order
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Access</strong>: Available in blocks after the loop completes
|
<strong>Access</strong>: Available in blocks after the loop
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -76,44 +76,11 @@ Choose between two types of parallel execution:
|
|||||||
3. Drag a single block inside the parallel container
|
3. Drag a single block inside the parallel container
|
||||||
4. Connect the block as needed
|
4. Connect the block as needed
|
||||||
|
|
||||||
### Referencing Parallel Data
|
### Accessing Results
|
||||||
|
|
||||||
There's an important distinction between referencing parallel data from **inside** vs **outside** the parallel block:
|
After a parallel block completes, you can access aggregated results:
|
||||||
|
|
||||||
<Tabs items={['Inside the Parallel', 'Outside the Parallel']}>
|
- **`<parallel.results>`**: Array of results from all parallel instances
|
||||||
<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
|
## Example Use Cases
|
||||||
|
|
||||||
@@ -131,11 +98,11 @@ Parallel (["gpt-4o", "claude-3.7-sonnet", "gemini-2.5-pro"]) → Agent → Evalu
|
|||||||
|
|
||||||
### Result Aggregation
|
### Result Aggregation
|
||||||
|
|
||||||
Results from all parallel instances are automatically collected and accessible via the block name:
|
Results from all parallel instances are automatically collected:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// In a Function block after a parallel named "Process Tasks"
|
// In a Function block after the parallel
|
||||||
const allResults = <processtasks.results>;
|
const allResults = input.parallel.results;
|
||||||
// Returns: [result1, result2, result3, ...]
|
// Returns: [result1, result2, result3, ...]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -191,26 +158,25 @@ Understanding when to use each:
|
|||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
Available **inside** the parallel only:
|
|
||||||
<ul className="list-disc space-y-2 pl-6">
|
<ul className="list-disc space-y-2 pl-6">
|
||||||
<li>
|
<li>
|
||||||
<strong>{"<parallel.index>"}</strong>: Instance number (0-based)
|
<strong>parallel.currentItem</strong>: Item for this instance
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>{"<parallel.currentItem>"}</strong>: Item for this instance (collection-based only)
|
<strong>parallel.index</strong>: Instance number (0-based)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>{"<parallel.items>"}</strong>: Full collection (collection-based only)
|
<strong>parallel.items</strong>: Full collection (collection-based)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
<ul className="list-disc space-y-2 pl-6">
|
<ul className="list-disc space-y-2 pl-6">
|
||||||
<li>
|
<li>
|
||||||
<strong>{"<blockname.results>"}</strong>: Array of all instance results (accessed via block name)
|
<strong>parallel.results</strong>: Array of all instance results
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Access</strong>: Available in blocks after the parallel completes
|
<strong>Access</strong>: Available in blocks after the parallel
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -5,24 +5,44 @@ title: Copilot
|
|||||||
import { Callout } from 'fumadocs-ui/components/callout'
|
import { Callout } from 'fumadocs-ui/components/callout'
|
||||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||||
import { Image } from '@/components/ui/image'
|
import { Image } from '@/components/ui/image'
|
||||||
import { MessageCircle, Hammer, Zap, Globe, Paperclip, History, RotateCcw, Brain } from 'lucide-react'
|
import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react'
|
||||||
|
|
||||||
Copilot is your in-editor assistant that helps you build and edit workflows. It can:
|
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:
|
||||||
|
|
||||||
- **Explain**: Answer questions about Sim and your current workflow
|
- **Explain**: Answer questions about Sim and your current workflow
|
||||||
- **Guide**: Suggest edits and best practices
|
- **Guide**: Suggest edits and best practices
|
||||||
- **Build**: Add blocks, wire connections, and configure settings
|
- **Edit**: Make changes to blocks, connections, and settings when you approve
|
||||||
- **Debug**: Analyze execution issues and optimize performance
|
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
Copilot is a Sim-managed service. For self-hosted deployments:
|
Copilot is a Sim-managed service. For self-hosted deployments, generate a Copilot API key in the hosted app (sim.ai → Settings → Copilot)
|
||||||
1. Go to [sim.ai](https://sim.ai) → Settings → Copilot and generate a Copilot API key
|
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
|
2. Set `COPILOT_API_KEY` in your self-hosted environment to that value
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
## Modes
|
## Context Menu (@)
|
||||||
|
|
||||||
Switch between modes using the mode selector at the bottom of the input area.
|
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
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card
|
<Card
|
||||||
@@ -40,153 +60,113 @@ Switch between modes using the mode selector at the bottom of the input area.
|
|||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Hammer className="h-4 w-4 text-muted-foreground" />
|
<Package className="h-4 w-4 text-muted-foreground" />
|
||||||
Build
|
Agent
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="m-0 text-sm">
|
<div className="m-0 text-sm">
|
||||||
Workflow building mode. Copilot can add blocks, wire connections, edit configurations, and debug issues.
|
Build-and-edit mode. Copilot proposes specific edits (add blocks, wire variables, tweak settings) and applies them when you approve.
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Cards>
|
</Cards>
|
||||||
|
|
||||||
## Models
|
<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>
|
||||||
|
|
||||||
Select your preferred AI model using the model selector at the bottom right of the input area.
|
## Depth Levels
|
||||||
|
|
||||||
**Available Models:**
|
<Cards>
|
||||||
- Claude 4.5 Opus, Sonnet (default), Haiku
|
<Card
|
||||||
- GPT 5.2 Codex, Pro
|
title={
|
||||||
- Gemini 3 Pro
|
<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>
|
||||||
|
|
||||||
Choose based on your needs: faster models for simple tasks, more capable models for complex workflows.
|
### Mode Selection Interface
|
||||||
|
|
||||||
## Context Menu (@)
|
You can easily switch between different reasoning modes using the mode selector in the Copilot interface:
|
||||||
|
|
||||||
Use the `@` symbol to reference resources and give Copilot more context:
|
<Image
|
||||||
|
src="/static/copilot/copilot-models.png"
|
||||||
|
alt="Copilot mode selection showing Advanced mode with MAX toggle"
|
||||||
|
width={600}
|
||||||
|
height={300}
|
||||||
|
/>
|
||||||
|
|
||||||
| Reference | Description |
|
The interface allows you to:
|
||||||
|-----------|-------------|
|
- **Select reasoning level**: Choose from Fast, Auto, Advanced, or Behemoth
|
||||||
| **Chats** | Previous copilot conversations |
|
- **Enable MAX mode**: Toggle for maximum reasoning capabilities when you need the most thorough analysis
|
||||||
| **Workflows** | Any workflow in your workspace |
|
- **See mode descriptions**: Understand what each mode is optimized for
|
||||||
| **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 |
|
|
||||||
|
|
||||||
Type `@` in the input field to open the context menu, then search or browse to find what you need.
|
Choose your mode based on the complexity of your task - use Fast for simple questions and Behemoth for complex architectural changes.
|
||||||
|
|
||||||
## Slash Commands (/)
|
## Billing and Cost Calculation
|
||||||
|
|
||||||
Use slash commands for quick actions:
|
### How Costs Are Calculated
|
||||||
|
|
||||||
| Command | Description |
|
Copilot usage is billed per token from the underlying LLM:
|
||||||
|---------|-------------|
|
|
||||||
| `/fast` | Fast mode execution |
|
|
||||||
| `/research` | Research and exploration mode |
|
|
||||||
| `/actions` | Execute agent actions |
|
|
||||||
|
|
||||||
**Web Commands:**
|
- **Input tokens**: billed at the provider's base rate (**at-cost**)
|
||||||
|
- **Output tokens**: billed at **1.5×** the provider's base output rate
|
||||||
|
|
||||||
| Command | Description |
|
```javascript
|
||||||
|---------|-------------|
|
copilotCost = (inputTokens × inputPrice + outputTokens × (outputPrice × 1.5)) / 1,000,000
|
||||||
| `/search` | Search the web |
|
```
|
||||||
| `/read` | Read a specific URL |
|
|
||||||
| `/scrape` | Scrape web page content |
|
|
||||||
| `/crawl` | Crawl multiple pages |
|
|
||||||
|
|
||||||
Type `/` in the input field to see available commands.
|
| Component | Rate Applied |
|
||||||
|
|----------|----------------------|
|
||||||
|
| Input | inputPrice |
|
||||||
|
| Output | outputPrice × 1.5 |
|
||||||
|
|
||||||
## Chat Management
|
<Callout type="warning">
|
||||||
|
Pricing shown reflects rates as of September 4, 2025. Check provider documentation for current pricing.
|
||||||
### Starting a New Chat
|
</Callout>
|
||||||
|
|
||||||
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">
|
<Callout type="info">
|
||||||
See the [Cost Calculation page](/execution/costs) for billing details.
|
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.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ Speed up your workflow building with these keyboard shortcuts and mouse controls
|
|||||||
| `Mod` + `V` | Paste blocks |
|
| `Mod` + `V` | Paste blocks |
|
||||||
| `Delete` or `Backspace` | Delete selected blocks or edges |
|
| `Delete` or `Backspace` | Delete selected blocks or edges |
|
||||||
| `Shift` + `L` | Auto-layout canvas |
|
| `Shift` + `L` | Auto-layout canvas |
|
||||||
| `Mod` + `Shift` + `F` | Fit to view |
|
|
||||||
| `Mod` + `Shift` + `Enter` | Accept Copilot changes |
|
|
||||||
|
|
||||||
## Panel Navigation
|
## Panel Navigation
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"./introduction/index",
|
"./introduction/index",
|
||||||
"./getting-started/index",
|
"./getting-started/index",
|
||||||
"./quick-reference/index",
|
|
||||||
"triggers",
|
"triggers",
|
||||||
"blocks",
|
"blocks",
|
||||||
"tools",
|
"tools",
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
---
|
|
||||||
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 |
|
|
||||||
|
|
||||||
@@ -16,20 +16,12 @@ Deploy Sim on your own infrastructure with Docker or Kubernetes.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
| Resource | Small | Standard | Production |
|
| Resource | Minimum | Recommended |
|
||||||
|----------|-------|----------|------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 cores | 4 cores | 8+ cores |
|
| CPU | 2 cores | 4+ cores |
|
||||||
| RAM | 12 GB | 16 GB | 32+ GB |
|
| RAM | 12 GB | 16+ GB |
|
||||||
| Storage | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| Storage | 20 GB SSD | 50+ GB SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | Latest |
|
| Docker | 20.10+ | Latest |
|
||||||
|
|
||||||
**Small**: Development, testing, single user (1-5 users)
|
|
||||||
**Standard**: Teams (5-50 users), moderate workloads
|
|
||||||
**Production**: Large teams (50+ users), high availability, heavy workflow execution
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Resource requirements are driven by workflow execution (isolated-vm sandboxing), file processing (in-memory document parsing), and vector operations (pgvector). Memory is typically the constraining factor rather than CPU. Production telemetry shows the main app uses 4-8 GB average with peaks up to 12 GB under heavy load.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ Despliega Sim en tu propia infraestructura con Docker o Kubernetes.
|
|||||||
|
|
||||||
## Requisitos
|
## Requisitos
|
||||||
|
|
||||||
| Recurso | Pequeño | Estándar | Producción |
|
| Recurso | Mínimo | Recomendado |
|
||||||
|----------|---------|----------|------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 núcleos | 4 núcleos | 8+ núcleos |
|
| CPU | 2 núcleos | 4+ núcleos |
|
||||||
| RAM | 12 GB | 16 GB | 32+ GB |
|
| RAM | 12 GB | 16+ GB |
|
||||||
| Almacenamiento | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| Almacenamiento | 20 GB SSD | 50+ GB SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | Última versión |
|
| Docker | 20.10+ | Última versión |
|
||||||
|
|
||||||
**Pequeño**: Desarrollo, pruebas, usuario único (1-5 usuarios)
|
|
||||||
**Estándar**: Equipos (5-50 usuarios), cargas de trabajo moderadas
|
|
||||||
**Producción**: Equipos grandes (50+ usuarios), alta disponibilidad, ejecución intensiva de workflows
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Los requisitos de recursos están determinados por la ejecución de workflows (sandboxing isolated-vm), procesamiento de archivos (análisis de documentos en memoria) y operaciones vectoriales (pgvector). La memoria suele ser el factor limitante, no la CPU. La telemetría de producción muestra que la aplicación principal usa 4-8 GB en promedio con picos de hasta 12 GB bajo carga pesada.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Inicio rápido
|
## Inicio rápido
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ Déployez Sim sur votre propre infrastructure avec Docker ou Kubernetes.
|
|||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
|
|
||||||
| Ressource | Petit | Standard | Production |
|
| Ressource | Minimum | Recommandé |
|
||||||
|----------|-------|----------|------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 cœurs | 4 cœurs | 8+ cœurs |
|
| CPU | 2 cœurs | 4+ cœurs |
|
||||||
| RAM | 12 Go | 16 Go | 32+ Go |
|
| RAM | 12 Go | 16+ Go |
|
||||||
| Stockage | 20 Go SSD | 50 Go SSD | 100+ Go SSD |
|
| Stockage | 20 Go SSD | 50+ Go SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | Dernière version |
|
| Docker | 20.10+ | Dernière version |
|
||||||
|
|
||||||
**Petit** : Développement, tests, utilisateur unique (1-5 utilisateurs)
|
|
||||||
**Standard** : Équipes (5-50 utilisateurs), charges de travail modérées
|
|
||||||
**Production** : Grandes équipes (50+ utilisateurs), haute disponibilité, exécution intensive de workflows
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Les besoins en ressources sont déterminés par l'exécution des workflows (sandboxing isolated-vm), le traitement des fichiers (analyse de documents en mémoire) et les opérations vectorielles (pgvector). La mémoire est généralement le facteur limitant, pas le CPU. La télémétrie de production montre que l'application principale utilise 4-8 Go en moyenne avec des pics jusqu'à 12 Go sous forte charge.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Démarrage rapide
|
## Démarrage rapide
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ DockerまたはKubernetesを使用して、自社のインフラストラクチ
|
|||||||
|
|
||||||
## 要件
|
## 要件
|
||||||
|
|
||||||
| リソース | スモール | スタンダード | プロダクション |
|
| リソース | 最小 | 推奨 |
|
||||||
|----------|---------|-------------|----------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2コア | 4コア | 8+コア |
|
| CPU | 2コア | 4+コア |
|
||||||
| RAM | 12 GB | 16 GB | 32+ GB |
|
| RAM | 12 GB | 16+ GB |
|
||||||
| ストレージ | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| ストレージ | 20 GB SSD | 50+ GB SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | 最新版 |
|
| Docker | 20.10+ | 最新版 |
|
||||||
|
|
||||||
**スモール**: 開発、テスト、シングルユーザー(1-5ユーザー)
|
|
||||||
**スタンダード**: チーム(5-50ユーザー)、中程度のワークロード
|
|
||||||
**プロダクション**: 大規模チーム(50+ユーザー)、高可用性、高負荷ワークフロー実行
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
リソース要件は、ワークフロー実行(isolated-vmサンドボックス)、ファイル処理(メモリ内ドキュメント解析)、ベクトル演算(pgvector)によって決まります。CPUよりもメモリが制約要因となることが多いです。本番環境のテレメトリによると、メインアプリは平均4-8 GB、高負荷時は最大12 GBを使用します。
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## クイックスタート
|
## クイックスタート
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ import { Callout } from 'fumadocs-ui/components/callout'
|
|||||||
|
|
||||||
## 要求
|
## 要求
|
||||||
|
|
||||||
| 资源 | 小型 | 标准 | 生产环境 |
|
| 资源 | 最低要求 | 推荐配置 |
|
||||||
|----------|------|------|----------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 核 | 4 核 | 8+ 核 |
|
| CPU | 2 核 | 4 核及以上 |
|
||||||
| 内存 | 12 GB | 16 GB | 32+ GB |
|
| 内存 | 12 GB | 16 GB 及以上 |
|
||||||
| 存储 | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| 存储 | 20 GB SSD | 50 GB 及以上 SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | 最新版本 |
|
| Docker | 20.10+ | 最新版本 |
|
||||||
|
|
||||||
**小型**: 开发、测试、单用户(1-5 用户)
|
|
||||||
**标准**: 团队(5-50 用户)、中等工作负载
|
|
||||||
**生产环境**: 大型团队(50+ 用户)、高可用性、密集工作流执行
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
资源需求由工作流执行(isolated-vm 沙箱)、文件处理(内存中文档解析)和向量运算(pgvector)决定。内存通常是限制因素,而不是 CPU。生产遥测数据显示,主应用平均使用 4-8 GB,高负载时峰值可达 12 GB。
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { Eye, EyeOff } from 'lucide-react'
|
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -21,10 +22,8 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
|
|||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
|
||||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
|
||||||
|
|
||||||
const logger = createLogger('LoginForm')
|
const logger = createLogger('LoginForm')
|
||||||
|
|
||||||
@@ -106,7 +105,8 @@ export default function LoginPage({
|
|||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||||
const [showValidationError, setShowValidationError] = useState(false)
|
const [showValidationError, setShowValidationError] = useState(false)
|
||||||
const buttonClass = useBrandedButtonClass()
|
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
||||||
|
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
||||||
|
|
||||||
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
||||||
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
||||||
@@ -114,6 +114,7 @@ export default function LoginPage({
|
|||||||
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
||||||
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
|
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
|
||||||
const [isSubmittingReset, setIsSubmittingReset] = useState(false)
|
const [isSubmittingReset, setIsSubmittingReset] = useState(false)
|
||||||
|
const [isResetButtonHovered, setIsResetButtonHovered] = useState(false)
|
||||||
const [resetStatus, setResetStatus] = useState<{
|
const [resetStatus, setResetStatus] = useState<{
|
||||||
type: 'success' | 'error' | null
|
type: 'success' | 'error' | null
|
||||||
message: string
|
message: string
|
||||||
@@ -122,7 +123,6 @@ export default function LoginPage({
|
|||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||||
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
@@ -139,12 +139,32 @@ export default function LoginPage({
|
|||||||
|
|
||||||
const inviteFlow = searchParams.get('invite_flow') === 'true'
|
const inviteFlow = searchParams.get('invite_flow') === 'true'
|
||||||
setIsInviteFlow(inviteFlow)
|
setIsInviteFlow(inviteFlow)
|
||||||
|
}
|
||||||
|
|
||||||
const resetSuccess = searchParams.get('resetSuccess') === 'true'
|
const checkCustomBrand = () => {
|
||||||
if (resetSuccess) {
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
setResetSuccessMessage('Password reset successful. Please sign in with your new password.')
|
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
||||||
|
|
||||||
|
if (brandAccent && brandAccent !== '#6f3dfa') {
|
||||||
|
setButtonClass('branded-button-custom')
|
||||||
|
} else {
|
||||||
|
setButtonClass('branded-button-gradient')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkCustomBrand()
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkCustomBrand)
|
||||||
|
const observer = new MutationObserver(checkCustomBrand)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkCustomBrand)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -182,13 +202,6 @@ export default function LoginPage({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
const redirectToVerify = (emailToVerify: string) => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
sessionStorage.setItem('verificationEmail', emailToVerify)
|
|
||||||
}
|
|
||||||
router.push('/verify')
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData(e.currentTarget)
|
const formData = new FormData(e.currentTarget)
|
||||||
const emailRaw = formData.get('email') as string
|
const emailRaw = formData.get('email') as string
|
||||||
const email = emailRaw.trim().toLowerCase()
|
const email = emailRaw.trim().toLowerCase()
|
||||||
@@ -208,7 +221,6 @@ export default function LoginPage({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
|
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
|
||||||
let errorHandled = false
|
|
||||||
|
|
||||||
const result = await client.signIn.email(
|
const result = await client.signIn.email(
|
||||||
{
|
{
|
||||||
@@ -219,16 +231,11 @@ export default function LoginPage({
|
|||||||
{
|
{
|
||||||
onError: (ctx) => {
|
onError: (ctx) => {
|
||||||
logger.error('Login error:', ctx.error)
|
logger.error('Login error:', ctx.error)
|
||||||
|
|
||||||
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
|
|
||||||
errorHandled = true
|
|
||||||
redirectToVerify(email)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
errorHandled = true
|
|
||||||
const errorMessage: string[] = ['Invalid email or password']
|
const errorMessage: string[] = ['Invalid email or password']
|
||||||
|
|
||||||
|
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
ctx.error.code?.includes('BAD_REQUEST') ||
|
ctx.error.code?.includes('BAD_REQUEST') ||
|
||||||
ctx.error.message?.includes('Email and password sign in is not enabled')
|
ctx.error.message?.includes('Email and password sign in is not enabled')
|
||||||
@@ -264,7 +271,6 @@ export default function LoginPage({
|
|||||||
errorMessage.push('Too many requests. Please wait a moment before trying again.')
|
errorMessage.push('Too many requests. Please wait a moment before trying again.')
|
||||||
}
|
}
|
||||||
|
|
||||||
setResetSuccessMessage(null)
|
|
||||||
setPasswordErrors(errorMessage)
|
setPasswordErrors(errorMessage)
|
||||||
setShowValidationError(true)
|
setShowValidationError(true)
|
||||||
},
|
},
|
||||||
@@ -272,25 +278,15 @@ export default function LoginPage({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!result || result.error) {
|
if (!result || result.error) {
|
||||||
// Show error if not already handled by onError callback
|
|
||||||
if (!errorHandled) {
|
|
||||||
setResetSuccessMessage(null)
|
|
||||||
const errorMessage = result?.error?.message || 'Login failed. Please try again.'
|
|
||||||
setPasswordErrors([errorMessage])
|
|
||||||
setShowValidationError(true)
|
|
||||||
}
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear reset success message on successful login
|
|
||||||
setResetSuccessMessage(null)
|
|
||||||
|
|
||||||
// Explicit redirect fallback if better-auth doesn't redirect
|
|
||||||
router.push(safeCallbackUrl)
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
|
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
|
||||||
redirectToVerify(email)
|
if (typeof window !== 'undefined') {
|
||||||
|
sessionStorage.setItem('verificationEmail', email)
|
||||||
|
}
|
||||||
|
router.push('/verify')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,13 +400,6 @@ export default function LoginPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Password reset success message */}
|
|
||||||
{resetSuccessMessage && (
|
|
||||||
<div className={`${inter.className} mt-1 space-y-1 text-[#4CAF50] text-xs`}>
|
|
||||||
<p>{resetSuccessMessage}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Email/Password Form - show unless explicitly disabled */}
|
{/* Email/Password Form - show unless explicitly disabled */}
|
||||||
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
|
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
|
||||||
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
|
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
|
||||||
@@ -493,14 +482,24 @@ export default function LoginPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BrandedButton
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
|
onMouseEnter={() => setIsButtonHovered(true)}
|
||||||
|
onMouseLeave={() => setIsButtonHovered(false)}
|
||||||
|
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
loading={isLoading}
|
|
||||||
loadingText='Signing in'
|
|
||||||
>
|
>
|
||||||
Sign in
|
<span className='flex items-center gap-1'>
|
||||||
</BrandedButton>
|
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||||
|
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||||
|
{isButtonHovered ? (
|
||||||
|
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -611,15 +610,25 @@ export default function LoginPage({
|
|||||||
<p>{resetStatus.message}</p>
|
<p>{resetStatus.message}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<BrandedButton
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={handleForgotPassword}
|
onClick={handleForgotPassword}
|
||||||
|
onMouseEnter={() => setIsResetButtonHovered(true)}
|
||||||
|
onMouseLeave={() => setIsResetButtonHovered(false)}
|
||||||
|
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
||||||
disabled={isSubmittingReset}
|
disabled={isSubmittingReset}
|
||||||
loading={isSubmittingReset}
|
|
||||||
loadingText='Sending'
|
|
||||||
>
|
>
|
||||||
Send Reset Link
|
<span className='flex items-center gap-1'>
|
||||||
</BrandedButton>
|
{isSubmittingReset ? 'Sending...' : 'Send Reset Link'}
|
||||||
|
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||||
|
{isResetButtonHovered ? (
|
||||||
|
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Eye, EyeOff } from 'lucide-react'
|
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
|
||||||
|
|
||||||
interface RequestResetFormProps {
|
interface RequestResetFormProps {
|
||||||
email: string
|
email: string
|
||||||
@@ -27,6 +27,36 @@ export function RequestResetForm({
|
|||||||
statusMessage,
|
statusMessage,
|
||||||
className,
|
className,
|
||||||
}: RequestResetFormProps) {
|
}: RequestResetFormProps) {
|
||||||
|
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
||||||
|
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkCustomBrand = () => {
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
|
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
||||||
|
|
||||||
|
if (brandAccent && brandAccent !== '#6f3dfa') {
|
||||||
|
setButtonClass('branded-button-custom')
|
||||||
|
} else {
|
||||||
|
setButtonClass('branded-button-gradient')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCustomBrand()
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkCustomBrand)
|
||||||
|
const observer = new MutationObserver(checkCustomBrand)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkCustomBrand)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onSubmit(email)
|
onSubmit(email)
|
||||||
@@ -64,14 +94,24 @@ export function RequestResetForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BrandedButton
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
loading={isSubmitting}
|
onMouseEnter={() => setIsButtonHovered(true)}
|
||||||
loadingText='Sending'
|
onMouseLeave={() => setIsButtonHovered(false)}
|
||||||
|
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
||||||
>
|
>
|
||||||
Send Reset Link
|
<span className='flex items-center gap-1'>
|
||||||
</BrandedButton>
|
{isSubmitting ? 'Sending...' : 'Send Reset Link'}
|
||||||
|
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||||
|
{isButtonHovered ? (
|
||||||
|
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -98,6 +138,35 @@ export function SetNewPasswordForm({
|
|||||||
const [validationMessage, setValidationMessage] = useState('')
|
const [validationMessage, setValidationMessage] = useState('')
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||||
|
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
||||||
|
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkCustomBrand = () => {
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
|
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
||||||
|
|
||||||
|
if (brandAccent && brandAccent !== '#6f3dfa') {
|
||||||
|
setButtonClass('branded-button-custom')
|
||||||
|
} else {
|
||||||
|
setButtonClass('branded-button-gradient')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCustomBrand()
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkCustomBrand)
|
||||||
|
const observer = new MutationObserver(checkCustomBrand)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkCustomBrand)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -227,14 +296,24 @@ export function SetNewPasswordForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BrandedButton
|
<Button
|
||||||
type='submit'
|
|
||||||
disabled={isSubmitting || !token}
|
disabled={isSubmitting || !token}
|
||||||
loading={isSubmitting}
|
type='submit'
|
||||||
loadingText='Resetting'
|
onMouseEnter={() => setIsButtonHovered(true)}
|
||||||
|
onMouseLeave={() => setIsButtonHovered(false)}
|
||||||
|
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
||||||
>
|
>
|
||||||
Reset Password
|
<span className='flex items-center gap-1'>
|
||||||
</BrandedButton>
|
{isSubmitting ? 'Resetting...' : 'Reset Password'}
|
||||||
|
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||||
|
{isButtonHovered ? (
|
||||||
|
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { Suspense, useEffect, useState } from 'react'
|
import { Suspense, useEffect, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { Eye, EyeOff } from 'lucide-react'
|
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { client, useSession } from '@/lib/auth/auth-client'
|
import { client, useSession } from '@/lib/auth/auth-client'
|
||||||
@@ -13,10 +14,8 @@ import { cn } from '@/lib/core/utils/cn'
|
|||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
|
||||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
|
||||||
|
|
||||||
const logger = createLogger('SignupForm')
|
const logger = createLogger('SignupForm')
|
||||||
|
|
||||||
@@ -96,7 +95,8 @@ function SignupFormContent({
|
|||||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||||
const [redirectUrl, setRedirectUrl] = useState('')
|
const [redirectUrl, setRedirectUrl] = useState('')
|
||||||
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
||||||
const buttonClass = useBrandedButtonClass()
|
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
||||||
|
const [isButtonHovered, setIsButtonHovered] = useState(false)
|
||||||
|
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [nameErrors, setNameErrors] = useState<string[]>([])
|
const [nameErrors, setNameErrors] = useState<string[]>([])
|
||||||
@@ -126,6 +126,31 @@ function SignupFormContent({
|
|||||||
if (inviteFlowParam === 'true') {
|
if (inviteFlowParam === 'true') {
|
||||||
setIsInviteFlow(true)
|
setIsInviteFlow(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkCustomBrand = () => {
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
|
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
||||||
|
|
||||||
|
if (brandAccent && brandAccent !== '#6f3dfa') {
|
||||||
|
setButtonClass('branded-button-custom')
|
||||||
|
} else {
|
||||||
|
setButtonClass('branded-button-gradient')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCustomBrand()
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkCustomBrand)
|
||||||
|
const observer = new MutationObserver(checkCustomBrand)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkCustomBrand)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
const validatePassword = (passwordValue: string): string[] => {
|
const validatePassword = (passwordValue: string): string[] => {
|
||||||
@@ -475,14 +500,24 @@ function SignupFormContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BrandedButton
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
|
onMouseEnter={() => setIsButtonHovered(true)}
|
||||||
|
onMouseLeave={() => setIsButtonHovered(false)}
|
||||||
|
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
loading={isLoading}
|
|
||||||
loadingText='Creating account'
|
|
||||||
>
|
>
|
||||||
Create account
|
<span className='flex items-center gap-1'>
|
||||||
</BrandedButton>
|
{isLoading ? 'Creating account' : 'Create account'}
|
||||||
|
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||||
|
{isButtonHovered ? (
|
||||||
|
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { cn } from '@/lib/core/utils/cn'
|
|||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
|
||||||
|
|
||||||
const logger = createLogger('SSOForm')
|
const logger = createLogger('SSOForm')
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ export default function SSOForm() {
|
|||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||||
const buttonClass = useBrandedButtonClass()
|
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
||||||
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -91,6 +90,31 @@ export default function SSOForm() {
|
|||||||
setShowEmailValidationError(true)
|
setShowEmailValidationError(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkCustomBrand = () => {
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
|
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
||||||
|
|
||||||
|
if (brandAccent && brandAccent !== '#6f3dfa') {
|
||||||
|
setButtonClass('branded-button-custom')
|
||||||
|
} else {
|
||||||
|
setButtonClass('branded-button-gradient')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCustomBrand()
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkCustomBrand)
|
||||||
|
const observer = new MutationObserver(checkCustomBrand)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkCustomBrand)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { cn } from '@/lib/core/utils/cn'
|
|||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
import { useVerification } from '@/app/(auth)/verify/use-verification'
|
import { useVerification } from '@/app/(auth)/verify/use-verification'
|
||||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
|
||||||
|
|
||||||
interface VerifyContentProps {
|
interface VerifyContentProps {
|
||||||
hasEmailService: boolean
|
hasEmailService: boolean
|
||||||
@@ -59,7 +58,34 @@ function VerificationForm({
|
|||||||
setCountdown(30)
|
setCountdown(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttonClass = useBrandedButtonClass()
|
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkCustomBrand = () => {
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
|
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
||||||
|
|
||||||
|
if (brandAccent && brandAccent !== '#6f3dfa') {
|
||||||
|
setButtonClass('branded-button-custom')
|
||||||
|
} else {
|
||||||
|
setButtonClass('branded-button-gradient')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCustomBrand()
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkCustomBrand)
|
||||||
|
const observer = new MutationObserver(checkCustomBrand)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkCustomBrand)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRef, useState } from 'react'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
import { Textarea } from '@/components/emcn'
|
import { Textarea } from '@/components/emcn'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +18,6 @@ import { isHosted } from '@/lib/core/config/feature-flags'
|
|||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
|
||||||
import Footer from '@/app/(landing)/components/footer/footer'
|
import Footer from '@/app/(landing)/components/footer/footer'
|
||||||
import Nav from '@/app/(landing)/components/nav/nav'
|
import Nav from '@/app/(landing)/components/nav/nav'
|
||||||
|
|
||||||
@@ -493,17 +493,18 @@ export default function CareersPage() {
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className='flex justify-end pt-2'>
|
<div className='flex justify-end pt-2'>
|
||||||
<BrandedButton
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={isSubmitting || submitStatus === 'success'}
|
disabled={isSubmitting || submitStatus === 'success'}
|
||||||
loading={isSubmitting}
|
className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50'
|
||||||
loadingText='Submitting'
|
size='lg'
|
||||||
showArrow={false}
|
|
||||||
fullWidth={false}
|
|
||||||
className='min-w-[200px]'
|
|
||||||
>
|
>
|
||||||
{submitStatus === 'success' ? 'Submitted' : 'Submit Application'}
|
{isSubmitting
|
||||||
</BrandedButton>
|
? 'Submitting...'
|
||||||
|
: submitStatus === 'success'
|
||||||
|
? 'Submitted'
|
||||||
|
: 'Submit Application'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function StatusIndicator() {
|
|||||||
href={statusUrl}
|
href={statusUrl}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className={`flex min-w-[165px] items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
|
className={`flex items-center gap-[6px] whitespace-nowrap text-[12px] transition-colors ${STATUS_COLORS[status]}`}
|
||||||
aria-label={`System status: ${message}`}
|
aria-label={`System status: ${message}`}
|
||||||
>
|
>
|
||||||
<StatusDotIcon status={status} className='h-[6px] w-[6px]' aria-hidden='true' />
|
<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 { LandingNode } from './landing-canvas/landing-block/landing-node'
|
||||||
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
||||||
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
||||||
export type { SubBlockRowProps, TagProps } from './landing-canvas/landing-block/tag'
|
export type { TagProps } from './landing-canvas/landing-block/tag'
|
||||||
export { SubBlockRow, Tag } from './landing-canvas/landing-block/tag'
|
export { Tag } from './landing-canvas/landing-block/tag'
|
||||||
export type {
|
export type {
|
||||||
LandingBlockNode,
|
LandingBlockNode,
|
||||||
LandingCanvasProps,
|
LandingCanvasProps,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { BookIcon } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
SubBlockRow,
|
Tag,
|
||||||
type SubBlockRowProps,
|
type TagProps,
|
||||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data structure for a landing card component
|
* Data structure for a landing card component
|
||||||
* Matches the workflow block structure from the application
|
|
||||||
*/
|
*/
|
||||||
export interface LandingCardData {
|
export interface LandingCardData {
|
||||||
/** Icon element to display in the card header */
|
/** Icon element to display in the card header */
|
||||||
@@ -15,8 +15,8 @@ export interface LandingCardData {
|
|||||||
color: string | '#f6f6f6'
|
color: string | '#f6f6f6'
|
||||||
/** Name/title of the card */
|
/** Name/title of the card */
|
||||||
name: string
|
name: string
|
||||||
/** Optional subblock rows to display below the header */
|
/** Optional tags to display at the bottom of the card */
|
||||||
tags?: SubBlockRowProps[]
|
tags?: TagProps[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,8 +28,7 @@ export interface LandingBlockProps extends LandingCardData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing block component that displays a card with icon, name, and optional subblock rows
|
* Landing block component that displays a card with icon, name, and optional tags
|
||||||
* Styled to match the application's workflow blocks
|
|
||||||
* @param props - Component properties including icon, color, name, tags, and className
|
* @param props - Component properties including icon, color, name, tags, and className
|
||||||
* @returns A styled block card component
|
* @returns A styled block card component
|
||||||
*/
|
*/
|
||||||
@@ -40,37 +39,33 @@ export const LandingBlock = React.memo(function LandingBlock({
|
|||||||
tags,
|
tags,
|
||||||
className,
|
className,
|
||||||
}: LandingBlockProps) {
|
}: LandingBlockProps) {
|
||||||
const hasContentBelowHeader = tags && tags.length > 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`z-10 flex w-[250px] flex-col rounded-[8px] border border-[#E5E5E5] bg-white ${className ?? ''}`}
|
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)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Header - matches workflow-block.tsx header styling */}
|
<div className='flex w-full items-center justify-between'>
|
||||||
<div
|
<div className='flex items-center gap-2.5'>
|
||||||
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
|
<div
|
||||||
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
className='flex h-6 w-6 items-center justify-center rounded-[8px] text-white'
|
||||||
style={{ background: color as string }}
|
style={{ backgroundColor: color as string }}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<span className='truncate font-medium text-[#171717] text-[16px]' title={name}>
|
<p className='text-base text-card-foreground'>{name}</p>
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<BookIcon className='h-4 w-4 text-muted-foreground' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content - SubBlock Rows matching workflow-block.tsx */}
|
{tags && tags.length > 0 ? (
|
||||||
{hasContentBelowHeader && (
|
<div className='flex flex-wrap gap-2'>
|
||||||
<div className='flex flex-col gap-[8px] p-[8px]'>
|
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<SubBlockRow key={tag.label} icon={tag.icon} label={tag.label} />
|
<Tag key={tag.label} icon={tag.icon} label={tag.label} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,14 +7,9 @@ import {
|
|||||||
type LandingCardData,
|
type LandingCardData,
|
||||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
} 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
|
* React Flow node component for the landing canvas
|
||||||
* Styled to match the application's workflow blocks
|
* Includes CSS animations and connection handles
|
||||||
* @param props - Component properties containing node data
|
* @param props - Component properties containing node data
|
||||||
* @returns A React Flow compatible node component
|
* @returns A React Flow compatible node component
|
||||||
*/
|
*/
|
||||||
@@ -46,15 +41,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
type='target'
|
type='target'
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
style={{
|
style={{
|
||||||
width: '7px',
|
width: '12px',
|
||||||
height: '20px',
|
height: '12px',
|
||||||
background: '#D1D1D1',
|
background: '#FEFEFE',
|
||||||
border: 'none',
|
border: '1px solid #E5E5E5',
|
||||||
borderRadius: '2px 0 0 2px',
|
borderRadius: '50%',
|
||||||
top: `${HANDLE_Y_OFFSET}px`,
|
top: '50%',
|
||||||
left: '-7px',
|
left: '-20px',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 10,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
/>
|
/>
|
||||||
@@ -64,15 +59,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
type='source'
|
type='source'
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
style={{
|
style={{
|
||||||
width: '7px',
|
width: '12px',
|
||||||
height: '20px',
|
height: '12px',
|
||||||
background: '#D1D1D1',
|
background: '#FEFEFE',
|
||||||
border: 'none',
|
border: '1px solid #E5E5E5',
|
||||||
borderRadius: '0 2px 2px 0',
|
borderRadius: '50%',
|
||||||
top: `${HANDLE_Y_OFFSET}px`,
|
top: '50%',
|
||||||
right: '-7px',
|
right: '-20px',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 10,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface LoopBlockProps {
|
|||||||
/**
|
/**
|
||||||
* Loop block container component that provides a styled container
|
* Loop block container component that provides a styled container
|
||||||
* for grouping related elements with a dashed border
|
* for grouping related elements with a dashed border
|
||||||
* Styled to match the application's subflow containers
|
|
||||||
* @param props - Component properties including children and styling
|
* @param props - Component properties including children and styling
|
||||||
* @returns A styled loop container component
|
* @returns A styled loop container component
|
||||||
*/
|
*/
|
||||||
@@ -30,33 +29,33 @@ export const LoopBlock = React.memo(function LoopBlock({
|
|||||||
style={{
|
style={{
|
||||||
width: '1198px',
|
width: '1198px',
|
||||||
height: '528px',
|
height: '528px',
|
||||||
borderRadius: '8px',
|
borderRadius: '14px',
|
||||||
background: 'rgba(59, 130, 246, 0.08)',
|
background: 'rgba(59, 130, 246, 0.10)',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Custom dashed border with SVG - 8px border radius to match blocks */}
|
{/* Custom dashed border with SVG */}
|
||||||
<svg
|
<svg
|
||||||
className='pointer-events-none absolute inset-0 h-full w-full'
|
className='pointer-events-none absolute inset-0 h-full w-full'
|
||||||
style={{ borderRadius: '8px' }}
|
style={{ borderRadius: '14px' }}
|
||||||
preserveAspectRatio='none'
|
preserveAspectRatio='none'
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
className='landing-loop-animated-dash'
|
className='landing-loop-animated-dash'
|
||||||
d='M 1190 527.5
|
d='M 1183.5 527.5
|
||||||
L 8 527.5
|
L 14 527.5
|
||||||
A 7.5 7.5 0 0 1 0.5 520
|
A 13.5 13.5 0 0 1 0.5 514
|
||||||
L 0.5 8
|
L 0.5 14
|
||||||
A 7.5 7.5 0 0 1 8 0.5
|
A 13.5 13.5 0 0 1 14 0.5
|
||||||
L 1190 0.5
|
L 1183.5 0.5
|
||||||
A 7.5 7.5 0 0 1 1197.5 8
|
A 13.5 13.5 0 0 1 1197 14
|
||||||
L 1197.5 520
|
L 1197 514
|
||||||
A 7.5 7.5 0 0 1 1190 527.5 Z'
|
A 13.5 13.5 0 0 1 1183.5 527.5 Z'
|
||||||
fill='none'
|
fill='none'
|
||||||
stroke='#3B82F6'
|
stroke='#3B82F6'
|
||||||
strokeWidth='1'
|
strokeWidth='1'
|
||||||
strokeDasharray='8 8'
|
strokeDasharray='12 12'
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,52 +1,25 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties for a subblock row component
|
* Properties for a tag component
|
||||||
* Matches the SubBlockRow pattern from workflow-block.tsx
|
|
||||||
*/
|
*/
|
||||||
export interface SubBlockRowProps {
|
export interface TagProps {
|
||||||
/** Icon element to display (optional, for visual context) */
|
/** Icon element to display in the tag */
|
||||||
icon?: React.ReactNode
|
icon: React.ReactNode
|
||||||
/** Text label for the row title */
|
/** Text label for the tag */
|
||||||
label: string
|
label: string
|
||||||
/** Optional value to display on the right side */
|
|
||||||
value?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kept for backwards compatibility
|
* Tag component for displaying labeled icons in a compact format
|
||||||
|
* @param props - Tag properties including icon and label
|
||||||
|
* @returns A styled tag component
|
||||||
*/
|
*/
|
||||||
export type TagProps = SubBlockRowProps
|
export const Tag = React.memo(function Tag({ icon, label }: TagProps) {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 (
|
return (
|
||||||
<div className='flex items-center gap-[8px]'>
|
<div className='flex w-fit items-center gap-1 rounded-[8px] border border-gray-300 bg-white px-2 py-0.5'>
|
||||||
<span className='min-w-0 truncate text-[#888888] text-[14px] capitalize' title={title}>
|
<div className='h-3 w-3 text-muted-foreground'>{icon}</div>
|
||||||
{title}
|
<p className='text-muted-foreground text-xs leading-normal'>{label}</p>
|
||||||
</span>
|
|
||||||
{displayValue && (
|
|
||||||
<span
|
|
||||||
className='flex-1 truncate text-right text-[#171717] text-[14px]'
|
|
||||||
title={displayValue}
|
|
||||||
>
|
|
||||||
{displayValue}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Tag component - alias for SubBlockRow for backwards compatibility
|
|
||||||
*/
|
|
||||||
export const Tag = SubBlockRow
|
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ import { LandingFlow } from '@/app/(landing)/components/hero/components/landing-
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Visual constants for landing node dimensions
|
* Visual constants for landing node dimensions
|
||||||
* Matches BLOCK_DIMENSIONS from the application
|
|
||||||
*/
|
*/
|
||||||
export const CARD_WIDTH = 250
|
export const CARD_WIDTH = 256
|
||||||
export const CARD_HEIGHT = 100
|
export const CARD_HEIGHT = 92
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing block node with positioning information
|
* Landing block node with positioning information
|
||||||
|
|||||||
@@ -4,29 +4,33 @@ import React from 'react'
|
|||||||
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom edge component with animated dashed line
|
* Custom edge component with animated dotted line that floats between handles
|
||||||
* Styled to match the application's workflow edges with rectangular handles
|
|
||||||
* @param props - React Flow edge properties
|
* @param props - React Flow edge properties
|
||||||
* @returns An animated dashed edge component
|
* @returns An animated dotted edge component
|
||||||
*/
|
*/
|
||||||
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
||||||
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style } = props
|
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, data } =
|
||||||
|
props
|
||||||
|
|
||||||
// Adjust the connection points to connect flush with rectangular handles
|
// Adjust the connection points to create floating effect
|
||||||
// Handle width is 7px, positioned at -7px from edge
|
// 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
|
||||||
let adjustedSourceX = sourceX
|
let adjustedSourceX = sourceX
|
||||||
let adjustedTargetX = targetX
|
let adjustedTargetX = targetX
|
||||||
|
|
||||||
if (sourcePosition === Position.Right) {
|
if (sourcePosition === Position.Right) {
|
||||||
adjustedSourceX = sourceX + 1
|
adjustedSourceX = sourceX + handleRadius + floatingGap
|
||||||
} else if (sourcePosition === Position.Left) {
|
} else if (sourcePosition === Position.Left) {
|
||||||
adjustedSourceX = sourceX - 1
|
adjustedSourceX = sourceX - handleRadius - floatingGap
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetPosition === Position.Left) {
|
if (targetPosition === Position.Left) {
|
||||||
adjustedTargetX = targetX - 1
|
adjustedTargetX = targetX - handleRadius - floatingGap
|
||||||
} else if (targetPosition === Position.Right) {
|
} else if (targetPosition === Position.Right) {
|
||||||
adjustedTargetX = targetX + 1
|
adjustedTargetX = targetX + handleRadius + floatingGap
|
||||||
}
|
}
|
||||||
|
|
||||||
const [path] = getSmoothStepPath({
|
const [path] = getSmoothStepPath({
|
||||||
@@ -36,8 +40,8 @@ export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
|||||||
targetY,
|
targetY,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
borderRadius: 8,
|
borderRadius: 20,
|
||||||
offset: 16,
|
offset: 10,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ArrowUp, CodeIcon } from 'lucide-react'
|
import {
|
||||||
|
ArrowUp,
|
||||||
|
BinaryIcon,
|
||||||
|
BookIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
CodeIcon,
|
||||||
|
Globe2Icon,
|
||||||
|
MessageSquareIcon,
|
||||||
|
VariableIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { type Edge, type Node, Position } from 'reactflow'
|
import { type Edge, type Node, Position } from 'reactflow'
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +23,7 @@ import {
|
|||||||
JiraIcon,
|
JiraIcon,
|
||||||
LinearIcon,
|
LinearIcon,
|
||||||
NotionIcon,
|
NotionIcon,
|
||||||
|
OpenAIIcon,
|
||||||
OutlookIcon,
|
OutlookIcon,
|
||||||
PackageSearchIcon,
|
PackageSearchIcon,
|
||||||
PineconeIcon,
|
PineconeIcon,
|
||||||
@@ -55,56 +65,67 @@ const SERVICE_TEMPLATES = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing blocks for the canvas preview
|
* Landing blocks for the canvas preview
|
||||||
* Styled to match the application's workflow blocks with subblock rows
|
|
||||||
*/
|
*/
|
||||||
const LANDING_BLOCKS: LandingManualBlock[] = [
|
const LANDING_BLOCKS: LandingManualBlock[] = [
|
||||||
{
|
{
|
||||||
id: 'schedule',
|
id: 'schedule',
|
||||||
name: 'Schedule',
|
name: 'Schedule',
|
||||||
color: '#7B68EE',
|
color: '#7B68EE',
|
||||||
icon: <ScheduleIcon className='h-[16px] w-[16px] text-white' />,
|
icon: <ScheduleIcon className='h-4 w-4' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 8, y: 60 },
|
mobile: { x: 8, y: 60 },
|
||||||
tablet: { x: 40, y: 120 },
|
tablet: { x: 40, y: 120 },
|
||||||
desktop: { x: 60, y: 180 },
|
desktop: { x: 60, y: 180 },
|
||||||
},
|
},
|
||||||
tags: [{ label: 'Time: 09:00AM Daily' }, { label: 'Timezone: PST' }],
|
tags: [
|
||||||
|
{ icon: <CalendarIcon className='h-3 w-3' />, label: '09:00AM Daily' },
|
||||||
|
{ icon: <Globe2Icon className='h-3 w-3' />, label: 'PST' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'knowledge',
|
id: 'knowledge',
|
||||||
name: 'Knowledge',
|
name: 'Knowledge',
|
||||||
color: '#00B0B0',
|
color: '#00B0B0',
|
||||||
icon: <PackageSearchIcon className='h-[16px] w-[16px] text-white' />,
|
icon: <PackageSearchIcon className='h-4 w-4' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 120, y: 140 },
|
mobile: { x: 120, y: 140 },
|
||||||
tablet: { x: 220, y: 200 },
|
tablet: { x: 220, y: 200 },
|
||||||
desktop: { x: 420, y: 241 },
|
desktop: { x: 420, y: 241 },
|
||||||
},
|
},
|
||||||
tags: [{ label: 'Source: Product Vector DB' }, { label: 'Limit: 10' }],
|
tags: [
|
||||||
|
{ icon: <BookIcon className='h-3 w-3' />, label: 'Product Vector DB' },
|
||||||
|
{ icon: <BinaryIcon className='h-3 w-3' />, label: 'Limit: 10' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'agent',
|
id: 'agent',
|
||||||
name: 'Agent',
|
name: 'Agent',
|
||||||
color: '#802FFF',
|
color: '#802FFF',
|
||||||
icon: <AgentIcon className='h-[16px] w-[16px] text-white' />,
|
icon: <AgentIcon className='h-4 w-4' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 340, y: 60 },
|
mobile: { x: 340, y: 60 },
|
||||||
tablet: { x: 540, y: 120 },
|
tablet: { x: 540, y: 120 },
|
||||||
desktop: { x: 880, y: 142 },
|
desktop: { x: 880, y: 142 },
|
||||||
},
|
},
|
||||||
tags: [{ label: 'Model: gpt-5' }, { label: 'Prompt: You are a support ag...' }],
|
tags: [
|
||||||
|
{ icon: <OpenAIIcon className='h-3 w-3' />, label: 'gpt-5' },
|
||||||
|
{ icon: <MessageSquareIcon className='h-3 w-3' />, label: 'You are a support ag...' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'function',
|
id: 'function',
|
||||||
name: 'Function',
|
name: 'Function',
|
||||||
color: '#FF402F',
|
color: '#FF402F',
|
||||||
icon: <CodeIcon className='h-[16px] w-[16px] text-white' />,
|
icon: <CodeIcon className='h-4 w-4' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 480, y: 220 },
|
mobile: { x: 480, y: 220 },
|
||||||
tablet: { x: 740, y: 280 },
|
tablet: { x: 740, y: 280 },
|
||||||
desktop: { x: 880, y: 340 },
|
desktop: { x: 880, y: 340 },
|
||||||
},
|
},
|
||||||
tags: [{ label: 'Language: Python' }, { label: 'Code: time = "2025-09-01...' }],
|
tags: [
|
||||||
|
{ icon: <CodeIcon className='h-3 w-3' />, label: 'Python' },
|
||||||
|
{ icon: <VariableIcon className='h-3 w-3' />, label: 'time = "2025-09-01...' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ function PricingCard({
|
|||||||
*/
|
*/
|
||||||
export default function LandingPricing() {
|
export default function LandingPricing() {
|
||||||
return (
|
return (
|
||||||
<section id='pricing' className='px-4 pt-[23px] sm:px-0 sm:pt-[4px]' aria-label='Pricing plans'>
|
<section id='pricing' className='px-4 pt-[19px] sm:px-0 sm:pt-0' aria-label='Pricing plans'>
|
||||||
<h2 className='sr-only'>Pricing Plans</h2>
|
<h2 className='sr-only'>Pricing Plans</h2>
|
||||||
<div className='relative mx-auto w-full max-w-[1289px]'>
|
<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'>
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useBrandConfig } from '@/lib/branding/branding'
|
|||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||||
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
||||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
|
||||||
|
|
||||||
const logger = createLogger('nav')
|
const logger = createLogger('nav')
|
||||||
|
|
||||||
@@ -21,12 +20,11 @@ interface NavProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||||
const [githubStars, setGithubStars] = useState('26.1k')
|
const [githubStars, setGithubStars] = useState('25.1k')
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const brand = useBrandConfig()
|
const brand = useBrandConfig()
|
||||||
const buttonClass = useBrandedButtonClass()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (variant !== 'landing') return
|
if (variant !== 'landing') return
|
||||||
@@ -185,7 +183,7 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
|
|||||||
href='/signup'
|
href='/signup'
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
className={`${buttonClass} group inline-flex items-center justify-center gap-2 rounded-[10px] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white transition-all`}
|
className='group inline-flex items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[14px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all sm:text-[16px]'
|
||||||
aria-label='Get started with Sim - Sign up for free'
|
aria-label='Get started with Sim - Sign up for free'
|
||||||
prefetch={true}
|
prefetch={true}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { ArrowLeft, ChevronLeft } from 'lucide-react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
export function BackLink() {
|
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href='/studio'
|
|
||||||
className='group flex items-center gap-1 text-gray-600 text-sm hover:text-gray-900'
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
>
|
|
||||||
<span className='group-hover:-translate-x-0.5 inline-flex transition-transform duration-200'>
|
|
||||||
{isHovered ? (
|
|
||||||
<ArrowLeft className='h-4 w-4' aria-hidden='true' />
|
|
||||||
) : (
|
|
||||||
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
Back to Sim Studio
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
|||||||
import { FAQ } from '@/lib/blog/faq'
|
import { FAQ } from '@/lib/blog/faq'
|
||||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
||||||
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
|
||||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
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() {
|
export async function generateStaticParams() {
|
||||||
const posts = await getAllPostMeta()
|
const posts = await getAllPostMeta()
|
||||||
@@ -51,7 +48,9 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
|||||||
/>
|
/>
|
||||||
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
<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'>
|
<div className='mb-6'>
|
||||||
<BackLink />
|
<Link href='/studio' className='text-gray-600 text-sm hover:text-gray-900'>
|
||||||
|
← Back to Sim Studio
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
||||||
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
||||||
@@ -76,31 +75,28 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
|||||||
>
|
>
|
||||||
{post.title}
|
{post.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className='mt-4 flex items-center justify-between'>
|
<div className='mt-4 flex items-center gap-3'>
|
||||||
<div className='flex items-center gap-3'>
|
{(post.authors || [post.author]).map((a, idx) => (
|
||||||
{(post.authors || [post.author]).map((a, idx) => (
|
<div key={idx} className='flex items-center gap-2'>
|
||||||
<div key={idx} className='flex items-center gap-2'>
|
{a?.avatarUrl ? (
|
||||||
{a?.avatarUrl ? (
|
<Avatar className='size-6'>
|
||||||
<Avatar className='size-6'>
|
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
||||||
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
||||||
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
</Avatar>
|
||||||
</Avatar>
|
) : null}
|
||||||
) : null}
|
<Link
|
||||||
<Link
|
href={a?.url || '#'}
|
||||||
href={a?.url || '#'}
|
target='_blank'
|
||||||
target='_blank'
|
rel='noopener noreferrer author'
|
||||||
rel='noopener noreferrer author'
|
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
|
||||||
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
|
itemProp='author'
|
||||||
itemProp='author'
|
itemScope
|
||||||
itemScope
|
itemType='https://schema.org/Person'
|
||||||
itemType='https://schema.org/Person'
|
>
|
||||||
>
|
<span itemProp='name'>{a?.name}</span>
|
||||||
<span itemProp='name'>{a?.name}</span>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<ShareButton url={`${getBaseUrl()}/studio/${slug}`} title={post.title} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Share2 } from 'lucide-react'
|
|
||||||
import { Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
|
|
||||||
|
|
||||||
interface ShareButtonProps {
|
|
||||||
url: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShareButton({ url, title }: ShareButtonProps) {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
|
|
||||||
const handleCopyLink = async () => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(url)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => {
|
|
||||||
setCopied(false)
|
|
||||||
setOpen(false)
|
|
||||||
}, 1000)
|
|
||||||
} catch {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleShareTwitter = () => {
|
|
||||||
const tweetUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`
|
|
||||||
window.open(tweetUrl, '_blank', 'noopener,noreferrer')
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleShareLinkedIn = () => {
|
|
||||||
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`
|
|
||||||
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
variant='secondary'
|
|
||||||
size='sm'
|
|
||||||
colorScheme='inverted'
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<button
|
|
||||||
className='flex items-center gap-1.5 text-gray-600 text-sm hover:text-gray-900'
|
|
||||||
aria-label='Share this post'
|
|
||||||
>
|
|
||||||
<Share2 className='h-4 w-4' />
|
|
||||||
<span>Share</span>
|
|
||||||
</button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent align='end' minWidth={140}>
|
|
||||||
<PopoverItem onClick={handleCopyLink}>{copied ? 'Copied!' : 'Copy link'}</PopoverItem>
|
|
||||||
<PopoverItem onClick={handleShareTwitter}>Share on X</PopoverItem>
|
|
||||||
<PopoverItem onClick={handleShareLinkedIn}>Share on LinkedIn</PopoverItem>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,7 @@ export default async function StudioIndex({
|
|||||||
? filtered.sort((a, b) => {
|
? filtered.sort((a, b) => {
|
||||||
if (a.featured && !b.featured) return -1
|
if (a.featured && !b.featured) return -1
|
||||||
if (!a.featured && b.featured) return 1
|
if (!a.featured && b.featured) return 1
|
||||||
return new Date(b.date).getTime() - new Date(a.date).getTime()
|
return 0
|
||||||
})
|
})
|
||||||
: filtered
|
: filtered
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
|||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { getRedisClient } from '@/lib/core/config/redis'
|
import { getRedisClient } from '@/lib/core/config/redis'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
|
||||||
|
|
||||||
const logger = createLogger('A2AAgentCardAPI')
|
const logger = createLogger('A2AAgentCardAPI')
|
||||||
|
|
||||||
@@ -96,11 +95,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<Ro
|
|||||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
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 body = await request.json()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -166,11 +160,6 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
|||||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
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))
|
await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
|
||||||
|
|
||||||
logger.info(`Deleted A2A agent: ${agentId}`)
|
logger.info(`Deleted A2A agent: ${agentId}`)
|
||||||
@@ -205,11 +194,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
|||||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
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 body = await request.json()
|
||||||
const action = body.action as 'publish' | 'unpublish' | 'refresh'
|
const action = body.action as 'publish' | 'unpublish' | 'refresh'
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { getBrandConfig } from '@/lib/branding/branding'
|
import { getBrandConfig } from '@/lib/branding/branding'
|
||||||
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
|
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 { SSE_HEADERS } from '@/lib/core/utils/sse'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||||
@@ -1119,13 +1118,17 @@ async function handlePushNotificationSet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlValidation = validateExternalUrl(
|
try {
|
||||||
params.pushNotificationConfig.url,
|
const url = new URL(params.pushNotificationConfig.url)
|
||||||
'Push notification URL'
|
if (url.protocol !== 'https:') {
|
||||||
)
|
return NextResponse.json(
|
||||||
if (!urlValidation.isValid) {
|
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Push notification URL must use HTTPS'),
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, urlValidation.error || 'Invalid URL'),
|
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification URL'),
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, desc, eq, inArray } from 'drizzle-orm'
|
import { and, desc, eq, inArray } from 'drizzle-orm'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { refreshOAuthToken } from '@/lib/oauth'
|
import { refreshOAuthToken } from '@/lib/oauth'
|
||||||
import {
|
|
||||||
getMicrosoftRefreshTokenExpiry,
|
|
||||||
isMicrosoftProvider,
|
|
||||||
PROACTIVE_REFRESH_THRESHOLD_DAYS,
|
|
||||||
} from '@/lib/oauth/microsoft'
|
|
||||||
|
|
||||||
const logger = createLogger('OAuthUtilsAPI')
|
const logger = createLogger('OAuthUtilsAPI')
|
||||||
|
|
||||||
@@ -210,32 +205,15 @@ export async function refreshAccessTokenIfNeeded(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decide if we should refresh: token missing OR expired
|
// Decide if we should refresh: token missing OR expired
|
||||||
const accessTokenExpiresAt = credential.accessTokenExpiresAt
|
const expiresAt = credential.accessTokenExpiresAt
|
||||||
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
const shouldRefresh =
|
||||||
// Check if access token needs refresh (missing or expired)
|
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
|
||||||
const accessTokenNeedsRefresh =
|
|
||||||
!!credential.refreshToken &&
|
|
||||||
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
|
|
||||||
|
|
||||||
// Check if we should proactively refresh to prevent refresh token expiry
|
|
||||||
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
|
|
||||||
const proactiveRefreshThreshold = new Date(
|
|
||||||
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
|
|
||||||
)
|
|
||||||
const refreshTokenNeedsProactiveRefresh =
|
|
||||||
!!credential.refreshToken &&
|
|
||||||
isMicrosoftProvider(credential.providerId) &&
|
|
||||||
refreshTokenExpiresAt &&
|
|
||||||
refreshTokenExpiresAt <= proactiveRefreshThreshold
|
|
||||||
|
|
||||||
const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
|
|
||||||
|
|
||||||
const accessToken = credential.accessToken
|
const accessToken = credential.accessToken
|
||||||
|
|
||||||
if (shouldRefresh) {
|
if (shouldRefresh) {
|
||||||
logger.info(`[${requestId}] Refreshing token for credential`)
|
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
|
||||||
try {
|
try {
|
||||||
const refreshedToken = await refreshOAuthToken(
|
const refreshedToken = await refreshOAuthToken(
|
||||||
credential.providerId,
|
credential.providerId,
|
||||||
@@ -249,15 +227,11 @@ export async function refreshAccessTokenIfNeeded(
|
|||||||
userId: credential.userId,
|
userId: credential.userId,
|
||||||
hasRefreshToken: !!credential.refreshToken,
|
hasRefreshToken: !!credential.refreshToken,
|
||||||
})
|
})
|
||||||
if (!accessTokenNeedsRefresh && accessToken) {
|
|
||||||
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
|
|
||||||
return accessToken
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare update data
|
// Prepare update data
|
||||||
const updateData: Record<string, unknown> = {
|
const updateData: any = {
|
||||||
accessToken: refreshedToken.accessToken,
|
accessToken: refreshedToken.accessToken,
|
||||||
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000),
|
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -269,10 +243,6 @@ export async function refreshAccessTokenIfNeeded(
|
|||||||
updateData.refreshToken = refreshedToken.refreshToken
|
updateData.refreshToken = refreshedToken.refreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMicrosoftProvider(credential.providerId)) {
|
|
||||||
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the token in the database
|
// Update the token in the database
|
||||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||||
|
|
||||||
@@ -286,10 +256,6 @@ export async function refreshAccessTokenIfNeeded(
|
|||||||
credentialId,
|
credentialId,
|
||||||
userId: credential.userId,
|
userId: credential.userId,
|
||||||
})
|
})
|
||||||
if (!accessTokenNeedsRefresh && accessToken) {
|
|
||||||
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
|
|
||||||
return accessToken
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
} else if (!accessToken) {
|
} else if (!accessToken) {
|
||||||
@@ -311,27 +277,10 @@ export async function refreshTokenIfNeeded(
|
|||||||
credentialId: string
|
credentialId: string
|
||||||
): Promise<{ accessToken: string; refreshed: boolean }> {
|
): Promise<{ accessToken: string; refreshed: boolean }> {
|
||||||
// Decide if we should refresh: token missing OR expired
|
// Decide if we should refresh: token missing OR expired
|
||||||
const accessTokenExpiresAt = credential.accessTokenExpiresAt
|
const expiresAt = credential.accessTokenExpiresAt
|
||||||
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
const shouldRefresh =
|
||||||
// Check if access token needs refresh (missing or expired)
|
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
|
||||||
const accessTokenNeedsRefresh =
|
|
||||||
!!credential.refreshToken &&
|
|
||||||
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
|
|
||||||
|
|
||||||
// Check if we should proactively refresh to prevent refresh token expiry
|
|
||||||
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
|
|
||||||
const proactiveRefreshThreshold = new Date(
|
|
||||||
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
|
|
||||||
)
|
|
||||||
const refreshTokenNeedsProactiveRefresh =
|
|
||||||
!!credential.refreshToken &&
|
|
||||||
isMicrosoftProvider(credential.providerId) &&
|
|
||||||
refreshTokenExpiresAt &&
|
|
||||||
refreshTokenExpiresAt <= proactiveRefreshThreshold
|
|
||||||
|
|
||||||
const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
|
|
||||||
|
|
||||||
// If token appears valid and present, return it directly
|
// If token appears valid and present, return it directly
|
||||||
if (!shouldRefresh) {
|
if (!shouldRefresh) {
|
||||||
@@ -344,17 +293,13 @@ export async function refreshTokenIfNeeded(
|
|||||||
|
|
||||||
if (!refreshResult) {
|
if (!refreshResult) {
|
||||||
logger.error(`[${requestId}] Failed to refresh token for credential`)
|
logger.error(`[${requestId}] Failed to refresh token for credential`)
|
||||||
if (!accessTokenNeedsRefresh && credential.accessToken) {
|
|
||||||
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
|
|
||||||
return { accessToken: credential.accessToken, refreshed: false }
|
|
||||||
}
|
|
||||||
throw new Error('Failed to refresh token')
|
throw new Error('Failed to refresh token')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult
|
const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult
|
||||||
|
|
||||||
// Prepare update data
|
// Prepare update data
|
||||||
const updateData: Record<string, unknown> = {
|
const updateData: any = {
|
||||||
accessToken: refreshedToken,
|
accessToken: refreshedToken,
|
||||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
|
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -366,10 +311,6 @@ export async function refreshTokenIfNeeded(
|
|||||||
updateData.refreshToken = newRefreshToken
|
updateData.refreshToken = newRefreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMicrosoftProvider(credential.providerId)) {
|
|
||||||
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||||
|
|
||||||
logger.info(`[${requestId}] Successfully refreshed access token`)
|
logger.info(`[${requestId}] Successfully refreshed access token`)
|
||||||
@@ -390,11 +331,6 @@ export async function refreshTokenIfNeeded(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessTokenNeedsRefresh && credential.accessToken) {
|
|
||||||
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
|
|
||||||
return { accessToken: credential.accessToken, refreshed: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Refresh failed and no valid token found in DB`, error)
|
logger.error(`[${requestId}] Refresh failed and no valid token found in DB`, error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ const resetPasswordSchema = z.object({
|
|||||||
.max(100, 'Password must not exceed 100 characters')
|
.max(100, 'Password must not exceed 100 characters')
|
||||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
.regex(/[0-9]/, 'Password must contain at least one number'),
|
||||||
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
|
|||||||
@@ -104,11 +104,17 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Build execution params starting with LLM-provided arguments
|
// Build execution params starting with LLM-provided arguments
|
||||||
// Resolve all {{ENV_VAR}} references in the arguments (deep for nested objects)
|
// Resolve all {{ENV_VAR}} references in the arguments
|
||||||
const executionParams: Record<string, any> = resolveEnvVarReferences(
|
const executionParams: Record<string, any> = resolveEnvVarReferences(
|
||||||
toolArgs,
|
toolArgs,
|
||||||
decryptedEnvVars,
|
decryptedEnvVars,
|
||||||
{ deep: true }
|
{
|
||||||
|
resolveExactMatch: true,
|
||||||
|
allowEmbedded: true,
|
||||||
|
trimKeys: true,
|
||||||
|
onMissing: 'keep',
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
) as Record<string, any>
|
) as Record<string, any>
|
||||||
|
|
||||||
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {
|
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {
|
||||||
|
|||||||
@@ -84,14 +84,6 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
|
|||||||
|
|
||||||
vi.mock('@sim/logger', () => loggerMock)
|
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', () => ({
|
vi.mock('@/lib/execution/e2b', () => ({
|
||||||
executeInE2B: vi.fn(),
|
executeInE2B: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@@ -118,24 +110,6 @@ describe('Function Execute API Route', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Security Tests', () => {
|
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 () => {
|
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return "test"',
|
code: 'return "test"',
|
||||||
@@ -302,11 +276,8 @@ describe('Function Execute API Route', () => {
|
|||||||
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
|
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <email>',
|
code: 'return <email>',
|
||||||
blockData: {
|
params: {
|
||||||
'block-123': { id: '123', subject: 'Test Email' },
|
email: { id: '123', subject: 'Test Email' },
|
||||||
},
|
|
||||||
blockNameMapping: {
|
|
||||||
email: 'block-123',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -334,13 +305,9 @@ describe('Function Execute API Route', () => {
|
|||||||
it.concurrent('should only match valid variable names in angle brackets', async () => {
|
it.concurrent('should only match valid variable names in angle brackets', async () => {
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
|
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
|
||||||
blockData: {
|
params: {
|
||||||
'block-1': 'hello',
|
validVar: 'hello',
|
||||||
'block-2': 'world',
|
another_valid: 'world',
|
||||||
},
|
|
||||||
blockNameMapping: {
|
|
||||||
validvar: 'block-1',
|
|
||||||
another_valid: 'block-2',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -354,22 +321,28 @@ describe('Function Execute API Route', () => {
|
|||||||
it.concurrent(
|
it.concurrent(
|
||||||
'should handle Gmail webhook data with email addresses containing angle brackets',
|
'should handle Gmail webhook data with email addresses containing angle brackets',
|
||||||
async () => {
|
async () => {
|
||||||
const emailData = {
|
const gmailData = {
|
||||||
id: '123',
|
email: {
|
||||||
from: 'Waleed Latif <waleed@sim.ai>',
|
id: '123',
|
||||||
to: 'User <user@example.com>',
|
from: 'Waleed Latif <waleed@sim.ai>',
|
||||||
subject: 'Test Email',
|
to: 'User <user@example.com>',
|
||||||
bodyText: 'Hello world',
|
subject: 'Test Email',
|
||||||
|
bodyText: 'Hello world',
|
||||||
|
},
|
||||||
|
rawEmail: {
|
||||||
|
id: '123',
|
||||||
|
payload: {
|
||||||
|
headers: [
|
||||||
|
{ name: 'From', value: 'Waleed Latif <waleed@sim.ai>' },
|
||||||
|
{ name: 'To', value: 'User <user@example.com>' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <email>',
|
code: 'return <email>',
|
||||||
blockData: {
|
params: gmailData,
|
||||||
'block-email': emailData,
|
|
||||||
},
|
|
||||||
blockNameMapping: {
|
|
||||||
email: 'block-email',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await POST(req)
|
const response = await POST(req)
|
||||||
@@ -383,20 +356,17 @@ describe('Function Execute API Route', () => {
|
|||||||
it.concurrent(
|
it.concurrent(
|
||||||
'should properly serialize complex email objects with special characters',
|
'should properly serialize complex email objects with special characters',
|
||||||
async () => {
|
async () => {
|
||||||
const emailData = {
|
const complexEmailData = {
|
||||||
from: 'Test User <test@example.com>',
|
email: {
|
||||||
bodyHtml: '<div>HTML content with "quotes" and \'apostrophes\'</div>',
|
from: 'Test User <test@example.com>',
|
||||||
bodyText: 'Text with\nnewlines\tand\ttabs',
|
bodyHtml: '<div>HTML content with "quotes" and \'apostrophes\'</div>',
|
||||||
|
bodyText: 'Text with\nnewlines\tand\ttabs',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <email>',
|
code: 'return <email>',
|
||||||
blockData: {
|
params: complexEmailData,
|
||||||
'block-email': emailData,
|
|
||||||
},
|
|
||||||
blockNameMapping: {
|
|
||||||
email: 'block-email',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await POST(req)
|
const response = await POST(req)
|
||||||
@@ -549,23 +519,18 @@ describe('Function Execute API Route', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should handle JSON serialization edge cases', async () => {
|
it.concurrent('should handle JSON serialization edge cases', async () => {
|
||||||
const complexData = {
|
|
||||||
special: 'chars"with\'quotes',
|
|
||||||
unicode: '🎉 Unicode content',
|
|
||||||
nested: {
|
|
||||||
deep: {
|
|
||||||
value: 'test',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = createMockRequest('POST', {
|
const req = createMockRequest('POST', {
|
||||||
code: 'return <complexData>',
|
code: 'return <complexData>',
|
||||||
blockData: {
|
params: {
|
||||||
'block-complex': complexData,
|
complexData: {
|
||||||
},
|
special: 'chars"with\'quotes',
|
||||||
blockNameMapping: {
|
unicode: '🎉 Unicode content',
|
||||||
complexdata: 'block-complex',
|
nested: {
|
||||||
|
deep: {
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
|
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { executeInE2B } from '@/lib/execution/e2b'
|
import { executeInE2B } from '@/lib/execution/e2b'
|
||||||
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
|
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
|
||||||
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
||||||
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
|
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
|
||||||
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
|
|
||||||
import {
|
import {
|
||||||
createEnvVarPattern,
|
createEnvVarPattern,
|
||||||
createWorkflowVariablePattern,
|
createWorkflowVariablePattern,
|
||||||
|
resolveEnvVarReferences,
|
||||||
} from '@/executor/utils/reference-validation'
|
} from '@/executor/utils/reference-validation'
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
export const runtime = 'nodejs'
|
export const runtime = 'nodejs'
|
||||||
@@ -19,8 +18,8 @@ export const MAX_DURATION = 210
|
|||||||
|
|
||||||
const logger = createLogger('FunctionExecuteAPI')
|
const logger = createLogger('FunctionExecuteAPI')
|
||||||
|
|
||||||
const E2B_JS_WRAPPER_LINES = 3
|
const E2B_JS_WRAPPER_LINES = 3 // Lines before user code: ';(async () => {', ' try {', ' const __sim_result = await (async () => {'
|
||||||
const E2B_PYTHON_WRAPPER_LINES = 1
|
const E2B_PYTHON_WRAPPER_LINES = 1 // Lines before user code: 'def __sim_main__():'
|
||||||
|
|
||||||
type TypeScriptModule = typeof import('typescript')
|
type TypeScriptModule = typeof import('typescript')
|
||||||
|
|
||||||
@@ -135,21 +134,33 @@ function extractEnhancedError(
|
|||||||
if (error.stack) {
|
if (error.stack) {
|
||||||
enhanced.stack = error.stack
|
enhanced.stack = error.stack
|
||||||
|
|
||||||
|
// Parse stack trace to extract line and column information
|
||||||
|
// Handle both compilation errors and runtime errors
|
||||||
const stackLines: string[] = error.stack.split('\n')
|
const stackLines: string[] = error.stack.split('\n')
|
||||||
|
|
||||||
for (const line of stackLines) {
|
for (const line of stackLines) {
|
||||||
|
// Pattern 1: Compilation errors - "user-function.js:6"
|
||||||
let match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
let match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||||
|
|
||||||
|
// Pattern 2: Runtime errors - "at user-function.js:5:12"
|
||||||
if (!match) {
|
if (!match) {
|
||||||
match = line.match(/at\s+user-function\.js:(\d+):(\d+)/)
|
match = line.match(/at\s+user-function\.js:(\d+):(\d+)/)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pattern 3: Generic patterns for any line containing our filename
|
||||||
|
if (!match) {
|
||||||
|
match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||||
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const stackLine = Number.parseInt(match[1], 10)
|
const stackLine = Number.parseInt(match[1], 10)
|
||||||
const stackColumn = match[2] ? Number.parseInt(match[2], 10) : undefined
|
const stackColumn = match[2] ? Number.parseInt(match[2], 10) : undefined
|
||||||
|
|
||||||
|
// Adjust line number to account for wrapper code
|
||||||
|
// The user code starts at a specific line in our wrapper
|
||||||
const adjustedLine = stackLine - userCodeStartLine + 1
|
const adjustedLine = stackLine - userCodeStartLine + 1
|
||||||
|
|
||||||
|
// Check if this is a syntax error in wrapper code caused by incomplete user code
|
||||||
const isWrapperSyntaxError =
|
const isWrapperSyntaxError =
|
||||||
stackLine > userCodeStartLine &&
|
stackLine > userCodeStartLine &&
|
||||||
error.name === 'SyntaxError' &&
|
error.name === 'SyntaxError' &&
|
||||||
@@ -157,6 +168,7 @@ function extractEnhancedError(
|
|||||||
error.message.includes('Unexpected end of input'))
|
error.message.includes('Unexpected end of input'))
|
||||||
|
|
||||||
if (isWrapperSyntaxError && userCode) {
|
if (isWrapperSyntaxError && userCode) {
|
||||||
|
// Map wrapper syntax errors to the last line of user code
|
||||||
const codeLines = userCode.split('\n')
|
const codeLines = userCode.split('\n')
|
||||||
const lastUserLine = codeLines.length
|
const lastUserLine = codeLines.length
|
||||||
enhanced.line = lastUserLine
|
enhanced.line = lastUserLine
|
||||||
@@ -169,6 +181,7 @@ function extractEnhancedError(
|
|||||||
enhanced.line = adjustedLine
|
enhanced.line = adjustedLine
|
||||||
enhanced.column = stackColumn
|
enhanced.column = stackColumn
|
||||||
|
|
||||||
|
// Extract the actual line content from user code
|
||||||
if (userCode) {
|
if (userCode) {
|
||||||
const codeLines = userCode.split('\n')
|
const codeLines = userCode.split('\n')
|
||||||
if (adjustedLine <= codeLines.length) {
|
if (adjustedLine <= codeLines.length) {
|
||||||
@@ -179,6 +192,7 @@ function extractEnhancedError(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stackLine <= userCodeStartLine) {
|
if (stackLine <= userCodeStartLine) {
|
||||||
|
// Error is in wrapper code itself
|
||||||
enhanced.line = stackLine
|
enhanced.line = stackLine
|
||||||
enhanced.column = stackColumn
|
enhanced.column = stackColumn
|
||||||
break
|
break
|
||||||
@@ -186,6 +200,7 @@ function extractEnhancedError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up stack trace to show user-relevant information
|
||||||
const cleanedStackLines: string[] = stackLines
|
const cleanedStackLines: string[] = stackLines
|
||||||
.filter(
|
.filter(
|
||||||
(line: string) =>
|
(line: string) =>
|
||||||
@@ -199,6 +214,9 @@ function extractEnhancedError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep original message without adding error type prefix
|
||||||
|
// The error type will be added later in createUserFriendlyErrorMessage
|
||||||
|
|
||||||
return enhanced
|
return enhanced
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +231,7 @@ function formatE2BError(
|
|||||||
userCode: string,
|
userCode: string,
|
||||||
prologueLineCount: number
|
prologueLineCount: number
|
||||||
): { formattedError: string; cleanedOutput: string } {
|
): { formattedError: string; cleanedOutput: string } {
|
||||||
|
// Calculate line offset based on language and prologue
|
||||||
const wrapperLines =
|
const wrapperLines =
|
||||||
language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES
|
language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES
|
||||||
const totalOffset = prologueLineCount + wrapperLines
|
const totalOffset = prologueLineCount + wrapperLines
|
||||||
@@ -222,20 +241,27 @@ function formatE2BError(
|
|||||||
let cleanErrorMsg = ''
|
let cleanErrorMsg = ''
|
||||||
|
|
||||||
if (language === CodeLanguage.Python) {
|
if (language === CodeLanguage.Python) {
|
||||||
|
// Python error format: "Cell In[X], line Y" followed by error details
|
||||||
|
// Extract line number from the Cell reference
|
||||||
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
|
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
|
||||||
if (cellMatch) {
|
if (cellMatch) {
|
||||||
const originalLine = Number.parseInt(cellMatch[1], 10)
|
const originalLine = Number.parseInt(cellMatch[1], 10)
|
||||||
userLine = originalLine - totalOffset
|
userLine = originalLine - totalOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract clean error message from the error string
|
||||||
|
// Remove file references like "(detected at line X) (file.py, line Y)"
|
||||||
cleanErrorMsg = errorMessage
|
cleanErrorMsg = errorMessage
|
||||||
.replace(/\s*\(detected at line \d+\)/g, '')
|
.replace(/\s*\(detected at line \d+\)/g, '')
|
||||||
.replace(/\s*\([^)]+\.py, line \d+\)/g, '')
|
.replace(/\s*\([^)]+\.py, line \d+\)/g, '')
|
||||||
.trim()
|
.trim()
|
||||||
} else if (language === CodeLanguage.JavaScript) {
|
} else if (language === CodeLanguage.JavaScript) {
|
||||||
|
// JavaScript error format from E2B: "SyntaxError: /path/file.ts: Message. (line:col)\n\n 9 | ..."
|
||||||
|
// First, extract the error type and message from the first line
|
||||||
const firstLineEnd = errorMessage.indexOf('\n')
|
const firstLineEnd = errorMessage.indexOf('\n')
|
||||||
const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage
|
const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage
|
||||||
|
|
||||||
|
// Parse: "SyntaxError: /home/user/index.ts: Missing semicolon. (11:9)"
|
||||||
const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/)
|
const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/)
|
||||||
if (jsErrorMatch) {
|
if (jsErrorMatch) {
|
||||||
cleanErrorType = jsErrorMatch[1]
|
cleanErrorType = jsErrorMatch[1]
|
||||||
@@ -243,11 +269,13 @@ function formatE2BError(
|
|||||||
const originalLine = Number.parseInt(jsErrorMatch[3], 10)
|
const originalLine = Number.parseInt(jsErrorMatch[3], 10)
|
||||||
userLine = originalLine - totalOffset
|
userLine = originalLine - totalOffset
|
||||||
} else {
|
} else {
|
||||||
|
// Fallback: look for line number in the arrow pointer line (> 11 |)
|
||||||
const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m)
|
const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m)
|
||||||
if (arrowMatch) {
|
if (arrowMatch) {
|
||||||
const originalLine = Number.parseInt(arrowMatch[1], 10)
|
const originalLine = Number.parseInt(arrowMatch[1], 10)
|
||||||
userLine = originalLine - totalOffset
|
userLine = originalLine - totalOffset
|
||||||
}
|
}
|
||||||
|
// Try to extract error type and message
|
||||||
const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/)
|
const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/)
|
||||||
if (errorMatch) {
|
if (errorMatch) {
|
||||||
cleanErrorType = errorMatch[1]
|
cleanErrorType = errorMatch[1]
|
||||||
@@ -261,11 +289,13 @@ function formatE2BError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the final clean error message
|
||||||
const finalErrorMsg =
|
const finalErrorMsg =
|
||||||
cleanErrorType && cleanErrorMsg
|
cleanErrorType && cleanErrorMsg
|
||||||
? `${cleanErrorType}: ${cleanErrorMsg}`
|
? `${cleanErrorType}: ${cleanErrorMsg}`
|
||||||
: cleanErrorMsg || errorMessage
|
: cleanErrorMsg || errorMessage
|
||||||
|
|
||||||
|
// Format with line number if available
|
||||||
let formattedError = finalErrorMsg
|
let formattedError = finalErrorMsg
|
||||||
if (userLine && userLine > 0) {
|
if (userLine && userLine > 0) {
|
||||||
const codeLines = userCode.split('\n')
|
const codeLines = userCode.split('\n')
|
||||||
@@ -281,6 +311,7 @@ function formatE2BError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For stdout, just return the clean error message without the full traceback
|
||||||
const cleanedOutput = finalErrorMsg
|
const cleanedOutput = finalErrorMsg
|
||||||
|
|
||||||
return { formattedError, cleanedOutput }
|
return { formattedError, cleanedOutput }
|
||||||
@@ -296,6 +327,7 @@ function createUserFriendlyErrorMessage(
|
|||||||
): string {
|
): string {
|
||||||
let errorMessage = enhanced.message
|
let errorMessage = enhanced.message
|
||||||
|
|
||||||
|
// Add line information if available
|
||||||
if (enhanced.line !== undefined) {
|
if (enhanced.line !== undefined) {
|
||||||
let lineInfo = `Line ${enhanced.line}`
|
let lineInfo = `Line ${enhanced.line}`
|
||||||
|
|
||||||
@@ -306,14 +338,18 @@ function createUserFriendlyErrorMessage(
|
|||||||
|
|
||||||
errorMessage = `${lineInfo} - ${errorMessage}`
|
errorMessage = `${lineInfo} - ${errorMessage}`
|
||||||
} else {
|
} else {
|
||||||
|
// If no line number, try to extract it from stack trace for display
|
||||||
if (enhanced.stack) {
|
if (enhanced.stack) {
|
||||||
const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||||
if (stackMatch) {
|
if (stackMatch) {
|
||||||
const line = Number.parseInt(stackMatch[1], 10)
|
const line = Number.parseInt(stackMatch[1], 10)
|
||||||
let lineInfo = `Line ${line}`
|
let lineInfo = `Line ${line}`
|
||||||
|
|
||||||
|
// Try to get line content if we have userCode
|
||||||
if (userCode) {
|
if (userCode) {
|
||||||
const codeLines = userCode.split('\n')
|
const codeLines = userCode.split('\n')
|
||||||
|
// Note: stackMatch gives us VM line number, need to adjust
|
||||||
|
// This is a fallback case, so we might not have perfect line mapping
|
||||||
if (line <= codeLines.length) {
|
if (line <= codeLines.length) {
|
||||||
const lineContent = codeLines[line - 1]?.trim()
|
const lineContent = codeLines[line - 1]?.trim()
|
||||||
if (lineContent) {
|
if (lineContent) {
|
||||||
@@ -327,6 +363,7 @@ function createUserFriendlyErrorMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add error type prefix with consistent naming
|
||||||
if (enhanced.name !== 'Error') {
|
if (enhanced.name !== 'Error') {
|
||||||
const errorTypePrefix =
|
const errorTypePrefix =
|
||||||
enhanced.name === 'SyntaxError'
|
enhanced.name === 'SyntaxError'
|
||||||
@@ -337,6 +374,7 @@ function createUserFriendlyErrorMessage(
|
|||||||
? 'Reference Error'
|
? 'Reference Error'
|
||||||
: enhanced.name
|
: enhanced.name
|
||||||
|
|
||||||
|
// Only add prefix if not already present
|
||||||
if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) {
|
if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) {
|
||||||
errorMessage = `${errorTypePrefix}: ${errorMessage}`
|
errorMessage = `${errorTypePrefix}: ${errorMessage}`
|
||||||
}
|
}
|
||||||
@@ -345,6 +383,9 @@ function createUserFriendlyErrorMessage(
|
|||||||
return errorMessage
|
return errorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves workflow variables with <variable.name> syntax
|
||||||
|
*/
|
||||||
function resolveWorkflowVariables(
|
function resolveWorkflowVariables(
|
||||||
code: string,
|
code: string,
|
||||||
workflowVariables: Record<string, any>,
|
workflowVariables: Record<string, any>,
|
||||||
@@ -364,35 +405,39 @@ function resolveWorkflowVariables(
|
|||||||
while ((match = regex.exec(code)) !== null) {
|
while ((match = regex.exec(code)) !== null) {
|
||||||
const variableName = match[1].trim()
|
const variableName = match[1].trim()
|
||||||
|
|
||||||
|
// Find the variable by name (workflowVariables is indexed by ID, values are variable objects)
|
||||||
const foundVariable = Object.entries(workflowVariables).find(
|
const foundVariable = Object.entries(workflowVariables).find(
|
||||||
([_, variable]) => normalizeName(variable.name || '') === variableName
|
([_, variable]) => normalizeName(variable.name || '') === variableName
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!foundVariable) {
|
let variableValue: unknown = ''
|
||||||
const availableVars = Object.values(workflowVariables)
|
if (foundVariable) {
|
||||||
.map((v) => v.name)
|
const variable = foundVariable[1]
|
||||||
.filter(Boolean)
|
variableValue = variable.value
|
||||||
throw new Error(
|
|
||||||
`Variable "${variableName}" doesn't exist.` +
|
|
||||||
(availableVars.length > 0 ? ` Available: ${availableVars.join(', ')}` : '')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const variable = foundVariable[1]
|
if (variable.value !== undefined && variable.value !== null) {
|
||||||
let variableValue: unknown = variable.value
|
|
||||||
|
|
||||||
if (variable.value !== undefined && variable.value !== null) {
|
|
||||||
const type = variable.type === 'string' ? 'plain' : variable.type
|
|
||||||
|
|
||||||
if (type === 'number') {
|
|
||||||
variableValue = Number(variableValue)
|
|
||||||
} else if (type === 'boolean') {
|
|
||||||
variableValue = variableValue === 'true' || variableValue === true
|
|
||||||
} else if (type === 'json' && typeof variableValue === 'string') {
|
|
||||||
try {
|
try {
|
||||||
variableValue = JSON.parse(variableValue)
|
// Handle 'string' type the same as 'plain' for backward compatibility
|
||||||
|
const type = variable.type === 'string' ? 'plain' : variable.type
|
||||||
|
|
||||||
|
// For plain text, use exactly what's entered without modifications
|
||||||
|
if (type === 'plain' && typeof variableValue === 'string') {
|
||||||
|
// Use as-is for plain text
|
||||||
|
} else if (type === 'number') {
|
||||||
|
variableValue = Number(variableValue)
|
||||||
|
} else if (type === 'boolean') {
|
||||||
|
variableValue = variableValue === 'true' || variableValue === true
|
||||||
|
} else if (type === 'json') {
|
||||||
|
try {
|
||||||
|
variableValue =
|
||||||
|
typeof variableValue === 'string' ? JSON.parse(variableValue) : variableValue
|
||||||
|
} catch {
|
||||||
|
// Keep original value if JSON parsing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Keep as-is
|
// Fallback to original value on error
|
||||||
|
variableValue = variable.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,9 +450,11 @@ function resolveWorkflowVariables(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process replacements in reverse order to maintain correct indices
|
||||||
for (let i = replacements.length - 1; i >= 0; i--) {
|
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||||
const { match: matchStr, index, variableName, variableValue } = replacements[i]
|
const { match: matchStr, index, variableName, variableValue } = replacements[i]
|
||||||
|
|
||||||
|
// Use variable reference approach
|
||||||
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||||
contextVariables[safeVarName] = variableValue
|
contextVariables[safeVarName] = variableValue
|
||||||
resolvedCode =
|
resolvedCode =
|
||||||
@@ -417,6 +464,9 @@ function resolveWorkflowVariables(
|
|||||||
return resolvedCode
|
return resolvedCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves environment variables with {{var_name}} syntax
|
||||||
|
*/
|
||||||
function resolveEnvironmentVariables(
|
function resolveEnvironmentVariables(
|
||||||
code: string,
|
code: string,
|
||||||
params: Record<string, any>,
|
params: Record<string, any>,
|
||||||
@@ -432,28 +482,32 @@ function resolveEnvironmentVariables(
|
|||||||
|
|
||||||
const resolverVars: Record<string, string> = {}
|
const resolverVars: Record<string, string> = {}
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null) {
|
if (value) {
|
||||||
resolverVars[key] = String(value)
|
resolverVars[key] = String(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Object.entries(envVars).forEach(([key, value]) => {
|
Object.entries(envVars).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null) {
|
if (value) {
|
||||||
resolverVars[key] = value
|
resolverVars[key] = value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
while ((match = regex.exec(code)) !== null) {
|
while ((match = regex.exec(code)) !== null) {
|
||||||
const varName = match[1].trim()
|
const varName = match[1].trim()
|
||||||
|
const resolved = resolveEnvVarReferences(match[0], resolverVars, {
|
||||||
if (!(varName in resolverVars)) {
|
allowEmbedded: true,
|
||||||
continue
|
resolveExactMatch: true,
|
||||||
}
|
trimKeys: true,
|
||||||
|
onMissing: 'empty',
|
||||||
|
deep: false,
|
||||||
|
})
|
||||||
|
const varValue =
|
||||||
|
typeof resolved === 'string' ? resolved : resolved == null ? '' : String(resolved)
|
||||||
replacements.push({
|
replacements.push({
|
||||||
match: match[0],
|
match: match[0],
|
||||||
index: match.index,
|
index: match.index,
|
||||||
varName,
|
varName,
|
||||||
varValue: resolverVars[varName],
|
varValue: String(varValue),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,59 +523,64 @@ function resolveEnvironmentVariables(
|
|||||||
return resolvedCode
|
return resolvedCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves tags with <tag_name> syntax (including nested paths like <block.response.data>)
|
||||||
|
*/
|
||||||
function resolveTagVariables(
|
function resolveTagVariables(
|
||||||
code: string,
|
code: string,
|
||||||
blockData: Record<string, unknown>,
|
params: Record<string, any>,
|
||||||
|
blockData: Record<string, any>,
|
||||||
blockNameMapping: Record<string, string>,
|
blockNameMapping: Record<string, string>,
|
||||||
blockOutputSchemas: Record<string, OutputSchema>,
|
contextVariables: Record<string, any>
|
||||||
contextVariables: Record<string, unknown>,
|
|
||||||
language = 'javascript'
|
|
||||||
): string {
|
): string {
|
||||||
let resolvedCode = code
|
let resolvedCode = code
|
||||||
const undefinedLiteral = language === 'python' ? 'None' : 'undefined'
|
|
||||||
|
|
||||||
const tagPattern = new RegExp(
|
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'
|
'g'
|
||||||
)
|
)
|
||||||
const tagMatches = resolvedCode.match(tagPattern) || []
|
const tagMatches = resolvedCode.match(tagPattern) || []
|
||||||
|
|
||||||
for (const match of tagMatches) {
|
for (const match of tagMatches) {
|
||||||
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
|
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
|
||||||
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
|
|
||||||
const blockName = pathParts[0]
|
|
||||||
const fieldPath = pathParts.slice(1)
|
|
||||||
|
|
||||||
const result = resolveBlockReference(blockName, fieldPath, {
|
// Handle nested paths like "getrecord.response.data" or "function1.response.result"
|
||||||
blockNameMapping,
|
// First try params, then blockData directly, then try with block name mapping
|
||||||
blockData,
|
let tagValue = getNestedValue(params, tagName) || getNestedValue(blockData, tagName) || ''
|
||||||
blockOutputSchemas,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!result) {
|
// If not found and the path starts with a block name, try mapping the block name to ID
|
||||||
continue
|
if (!tagValue && tagName.includes(REFERENCE.PATH_DELIMITER)) {
|
||||||
}
|
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
|
||||||
|
const normalizedBlockName = pathParts[0] // This should already be normalized like "function1"
|
||||||
|
|
||||||
let tagValue = result.value
|
// Direct lookup using normalized block name
|
||||||
|
const blockId = blockNameMapping[normalizedBlockName] ?? null
|
||||||
|
|
||||||
if (tagValue === undefined) {
|
if (blockId) {
|
||||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral)
|
const remainingPath = pathParts.slice(1).join('.')
|
||||||
continue
|
const fullPath = `${blockId}.${remainingPath}`
|
||||||
}
|
tagValue = getNestedValue(blockData, fullPath) || ''
|
||||||
|
|
||||||
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(/_/g, '_1').replace(/\./g, '_0')}`
|
// If the value is a stringified JSON, parse it back to object
|
||||||
|
if (
|
||||||
|
typeof tagValue === 'string' &&
|
||||||
|
tagValue.length > 100 &&
|
||||||
|
(tagValue.startsWith('{') || tagValue.startsWith('['))
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
tagValue = JSON.parse(tagValue)
|
||||||
|
} catch (e) {
|
||||||
|
// Keep as string if parsing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of injecting large JSON directly, create a variable reference
|
||||||
|
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||||
contextVariables[safeVarName] = tagValue
|
contextVariables[safeVarName] = tagValue
|
||||||
|
|
||||||
|
// Replace the template with a variable reference
|
||||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,31 +596,44 @@ function resolveTagVariables(
|
|||||||
*/
|
*/
|
||||||
function resolveCodeVariables(
|
function resolveCodeVariables(
|
||||||
code: string,
|
code: string,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, any>,
|
||||||
envVars: Record<string, string> = {},
|
envVars: Record<string, string> = {},
|
||||||
blockData: Record<string, unknown> = {},
|
blockData: Record<string, any> = {},
|
||||||
blockNameMapping: Record<string, string> = {},
|
blockNameMapping: Record<string, string> = {},
|
||||||
blockOutputSchemas: Record<string, OutputSchema> = {},
|
workflowVariables: Record<string, any> = {}
|
||||||
workflowVariables: Record<string, unknown> = {},
|
): { resolvedCode: string; contextVariables: Record<string, any> } {
|
||||||
language = 'javascript'
|
|
||||||
): { resolvedCode: string; contextVariables: Record<string, unknown> } {
|
|
||||||
let resolvedCode = code
|
let resolvedCode = code
|
||||||
const contextVariables: Record<string, unknown> = {}
|
const contextVariables: Record<string, any> = {}
|
||||||
|
|
||||||
|
// Resolve workflow variables with <variable.name> syntax first
|
||||||
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
|
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
|
||||||
|
|
||||||
|
// Resolve environment variables with {{var_name}} syntax
|
||||||
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
|
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
|
||||||
|
|
||||||
|
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
|
||||||
resolvedCode = resolveTagVariables(
|
resolvedCode = resolveTagVariables(
|
||||||
resolvedCode,
|
resolvedCode,
|
||||||
|
params,
|
||||||
blockData,
|
blockData,
|
||||||
blockNameMapping,
|
blockNameMapping,
|
||||||
blockOutputSchemas,
|
contextVariables
|
||||||
contextVariables,
|
|
||||||
language
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return { resolvedCode, contextVariables }
|
return { resolvedCode, contextVariables }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get nested value from object using dot notation path
|
||||||
|
*/
|
||||||
|
function getNestedValue(obj: any, path: string): any {
|
||||||
|
if (!obj || !path) return undefined
|
||||||
|
|
||||||
|
return path.split('.').reduce((current, key) => {
|
||||||
|
return current && typeof current === 'object' ? current[key] : undefined
|
||||||
|
}, obj)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove one trailing newline from stdout
|
* Remove one trailing newline from stdout
|
||||||
* This handles the common case where print() or console.log() adds a trailing \n
|
* This handles the common case where print() or console.log() adds a trailing \n
|
||||||
@@ -582,12 +654,6 @@ export async function POST(req: NextRequest) {
|
|||||||
let resolvedCode = '' // Store resolved code for error reporting
|
let resolvedCode = '' // Store resolved code for error reporting
|
||||||
|
|
||||||
try {
|
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 body = await req.json()
|
||||||
|
|
||||||
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
|
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
|
||||||
@@ -600,12 +666,12 @@ export async function POST(req: NextRequest) {
|
|||||||
envVars = {},
|
envVars = {},
|
||||||
blockData = {},
|
blockData = {},
|
||||||
blockNameMapping = {},
|
blockNameMapping = {},
|
||||||
blockOutputSchemas = {},
|
|
||||||
workflowVariables = {},
|
workflowVariables = {},
|
||||||
workflowId,
|
workflowId,
|
||||||
isCustomTool = false,
|
isCustomTool = false,
|
||||||
} = body
|
} = body
|
||||||
|
|
||||||
|
// Extract internal parameters that shouldn't be passed to the execution context
|
||||||
const executionParams = { ...params }
|
const executionParams = { ...params }
|
||||||
executionParams._context = undefined
|
executionParams._context = undefined
|
||||||
|
|
||||||
@@ -617,21 +683,21 @@ export async function POST(req: NextRequest) {
|
|||||||
isCustomTool,
|
isCustomTool,
|
||||||
})
|
})
|
||||||
|
|
||||||
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
|
// Resolve variables in the code with workflow environment variables
|
||||||
|
|
||||||
const codeResolution = resolveCodeVariables(
|
const codeResolution = resolveCodeVariables(
|
||||||
code,
|
code,
|
||||||
executionParams,
|
executionParams,
|
||||||
envVars,
|
envVars,
|
||||||
blockData,
|
blockData,
|
||||||
blockNameMapping,
|
blockNameMapping,
|
||||||
blockOutputSchemas,
|
workflowVariables
|
||||||
workflowVariables,
|
|
||||||
lang
|
|
||||||
)
|
)
|
||||||
resolvedCode = codeResolution.resolvedCode
|
resolvedCode = codeResolution.resolvedCode
|
||||||
const contextVariables = codeResolution.contextVariables
|
const contextVariables = codeResolution.contextVariables
|
||||||
|
|
||||||
|
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
|
||||||
|
|
||||||
|
// Extract imports once for JavaScript code (reuse later to avoid double extraction)
|
||||||
let jsImports = ''
|
let jsImports = ''
|
||||||
let jsRemainingCode = resolvedCode
|
let jsRemainingCode = resolvedCode
|
||||||
let hasImports = false
|
let hasImports = false
|
||||||
@@ -641,22 +707,31 @@ export async function POST(req: NextRequest) {
|
|||||||
jsImports = extractionResult.imports
|
jsImports = extractionResult.imports
|
||||||
jsRemainingCode = extractionResult.remainingCode
|
jsRemainingCode = extractionResult.remainingCode
|
||||||
|
|
||||||
|
// Check for ES6 imports or CommonJS require statements
|
||||||
|
// ES6 imports are extracted by the TypeScript parser
|
||||||
|
// Also check for require() calls which indicate external dependencies
|
||||||
const hasRequireStatements = /require\s*\(\s*['"`]/.test(resolvedCode)
|
const hasRequireStatements = /require\s*\(\s*['"`]/.test(resolvedCode)
|
||||||
hasImports = jsImports.trim().length > 0 || hasRequireStatements
|
hasImports = jsImports.trim().length > 0 || hasRequireStatements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Python always requires E2B
|
||||||
if (lang === CodeLanguage.Python && !isE2bEnabled) {
|
if (lang === CodeLanguage.Python && !isE2bEnabled) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.'
|
'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JavaScript with imports requires E2B
|
||||||
if (lang === CodeLanguage.JavaScript && hasImports && !isE2bEnabled) {
|
if (lang === CodeLanguage.JavaScript && hasImports && !isE2bEnabled) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.'
|
'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use E2B if:
|
||||||
|
// - E2B is enabled AND
|
||||||
|
// - Not a custom tool AND
|
||||||
|
// - (Python OR JavaScript with imports)
|
||||||
const useE2B =
|
const useE2B =
|
||||||
isE2bEnabled &&
|
isE2bEnabled &&
|
||||||
!isCustomTool &&
|
!isCustomTool &&
|
||||||
@@ -669,10 +744,13 @@ export async function POST(req: NextRequest) {
|
|||||||
language: lang,
|
language: lang,
|
||||||
})
|
})
|
||||||
let prologue = ''
|
let prologue = ''
|
||||||
|
const epilogue = ''
|
||||||
|
|
||||||
if (lang === CodeLanguage.JavaScript) {
|
if (lang === CodeLanguage.JavaScript) {
|
||||||
|
// Track prologue lines for error adjustment
|
||||||
let prologueLineCount = 0
|
let prologueLineCount = 0
|
||||||
|
|
||||||
|
// Reuse the imports we already extracted earlier
|
||||||
const imports = jsImports
|
const imports = jsImports
|
||||||
const remainingCode = jsRemainingCode
|
const remainingCode = jsRemainingCode
|
||||||
|
|
||||||
@@ -687,11 +765,7 @@ export async function POST(req: NextRequest) {
|
|||||||
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
|
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
for (const [k, v] of Object.entries(contextVariables)) {
|
for (const [k, v] of Object.entries(contextVariables)) {
|
||||||
if (v === undefined) {
|
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
|
||||||
prologue += `const ${k} = undefined;\n`
|
|
||||||
} else {
|
|
||||||
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
|
|
||||||
}
|
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,7 +782,7 @@ export async function POST(req: NextRequest) {
|
|||||||
' }',
|
' }',
|
||||||
'})();',
|
'})();',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const codeForE2B = importSection + prologue + wrapped
|
const codeForE2B = importSection + prologue + wrapped + epilogue
|
||||||
|
|
||||||
const execStart = Date.now()
|
const execStart = Date.now()
|
||||||
const {
|
const {
|
||||||
@@ -730,6 +804,7 @@ export async function POST(req: NextRequest) {
|
|||||||
error: e2bError,
|
error: e2bError,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If there was an execution error, format it properly
|
||||||
if (e2bError) {
|
if (e2bError) {
|
||||||
const { formattedError, cleanedOutput } = formatE2BError(
|
const { formattedError, cleanedOutput } = formatE2BError(
|
||||||
e2bError,
|
e2bError,
|
||||||
@@ -753,7 +828,7 @@ export async function POST(req: NextRequest) {
|
|||||||
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
|
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// Track prologue lines for error adjustment
|
||||||
let prologueLineCount = 0
|
let prologueLineCount = 0
|
||||||
prologue += 'import json\n'
|
prologue += 'import json\n'
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
@@ -762,11 +837,7 @@ export async function POST(req: NextRequest) {
|
|||||||
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
|
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
for (const [k, v] of Object.entries(contextVariables)) {
|
for (const [k, v] of Object.entries(contextVariables)) {
|
||||||
if (v === undefined) {
|
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
|
||||||
prologue += `${k} = None\n`
|
|
||||||
} else {
|
|
||||||
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
|
|
||||||
}
|
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
}
|
}
|
||||||
const wrapped = [
|
const wrapped = [
|
||||||
@@ -775,7 +846,7 @@ export async function POST(req: NextRequest) {
|
|||||||
'__sim_result__ = __sim_main__()',
|
'__sim_result__ = __sim_main__()',
|
||||||
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
|
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const codeForE2B = prologue + wrapped
|
const codeForE2B = prologue + wrapped + epilogue
|
||||||
|
|
||||||
const execStart = Date.now()
|
const execStart = Date.now()
|
||||||
const {
|
const {
|
||||||
@@ -797,6 +868,7 @@ export async function POST(req: NextRequest) {
|
|||||||
error: e2bError,
|
error: e2bError,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If there was an execution error, format it properly
|
||||||
if (e2bError) {
|
if (e2bError) {
|
||||||
const { formattedError, cleanedOutput } = formatE2BError(
|
const { formattedError, cleanedOutput } = formatE2BError(
|
||||||
e2bError,
|
e2bError,
|
||||||
@@ -825,6 +897,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const wrapperLines = ['(async () => {', ' try {']
|
const wrapperLines = ['(async () => {', ' try {']
|
||||||
if (isCustomTool) {
|
if (isCustomTool) {
|
||||||
|
wrapperLines.push(' // For custom tools, make parameters directly accessible')
|
||||||
Object.keys(executionParams).forEach((key) => {
|
Object.keys(executionParams).forEach((key) => {
|
||||||
wrapperLines.push(` const ${key} = params.${key};`)
|
wrapperLines.push(` const ${key} = params.${key};`)
|
||||||
})
|
})
|
||||||
@@ -858,10 +931,12 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const ivmError = isolatedResult.error
|
const ivmError = isolatedResult.error
|
||||||
|
// Adjust line number for prepended param destructuring in custom tools
|
||||||
let adjustedLine = ivmError.line
|
let adjustedLine = ivmError.line
|
||||||
let adjustedLineContent = ivmError.lineContent
|
let adjustedLineContent = ivmError.lineContent
|
||||||
if (prependedLineCount > 0 && ivmError.line !== undefined) {
|
if (prependedLineCount > 0 && ivmError.line !== undefined) {
|
||||||
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
|
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
|
||||||
|
// Get line content from original user code, not the prepended code
|
||||||
const codeLines = resolvedCode.split('\n')
|
const codeLines = resolvedCode.split('\n')
|
||||||
if (adjustedLine <= codeLines.length) {
|
if (adjustedLine <= codeLines.length) {
|
||||||
adjustedLineContent = codeLines[adjustedLine - 1]?.trim()
|
adjustedLineContent = codeLines[adjustedLine - 1]?.trim()
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||||
'kb-123',
|
'kb-123',
|
||||||
{
|
{
|
||||||
enabledFilter: undefined,
|
includeDisabled: false,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -166,7 +166,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return documents with default filter', async () => {
|
it('should filter disabled documents by default', async () => {
|
||||||
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
||||||
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||||
'kb-123',
|
'kb-123',
|
||||||
{
|
{
|
||||||
enabledFilter: undefined,
|
includeDisabled: false,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -203,7 +203,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should filter documents by enabled status when requested', async () => {
|
it('should include disabled documents when requested', async () => {
|
||||||
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
const { checkKnowledgeBaseAccess } = await import('@/app/api/knowledge/utils')
|
||||||
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
const { getDocuments } = await import('@/lib/knowledge/documents/service')
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?enabledFilter=disabled'
|
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?includeDisabled=true'
|
||||||
const req = new Request(url, { method: 'GET' }) as any
|
const req = new Request(url, { method: 'GET' }) as any
|
||||||
|
|
||||||
const { GET } = await import('@/app/api/knowledge/[id]/documents/route')
|
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(
|
expect(vi.mocked(getDocuments)).toHaveBeenCalledWith(
|
||||||
'kb-123',
|
'kb-123',
|
||||||
{
|
{
|
||||||
enabledFilter: 'disabled',
|
includeDisabled: true,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -361,7 +361,8 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith(
|
expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith(
|
||||||
validDocumentData,
|
validDocumentData,
|
||||||
'kb-123',
|
'kb-123',
|
||||||
expect.any(String)
|
expect.any(String),
|
||||||
|
'user-123'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -469,7 +470,8 @@ describe('Knowledge Base Documents API Route', () => {
|
|||||||
expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith(
|
expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith(
|
||||||
validBulkData.documents,
|
validBulkData.documents,
|
||||||
'kb-123',
|
'kb-123',
|
||||||
expect.any(String)
|
expect.any(String),
|
||||||
|
'user-123'
|
||||||
)
|
)
|
||||||
expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled()
|
expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { z } from 'zod'
|
|||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import {
|
import {
|
||||||
bulkDocumentOperation,
|
bulkDocumentOperation,
|
||||||
bulkDocumentOperationByFilter,
|
|
||||||
createDocumentRecords,
|
createDocumentRecords,
|
||||||
createSingleDocument,
|
createSingleDocument,
|
||||||
getDocuments,
|
getDocuments,
|
||||||
@@ -58,20 +57,13 @@ const BulkCreateDocumentsSchema = z.object({
|
|||||||
bulk: z.literal(true),
|
bulk: z.literal(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
const BulkUpdateDocumentsSchema = z
|
const BulkUpdateDocumentsSchema = z.object({
|
||||||
.object({
|
operation: z.enum(['enable', 'disable', 'delete']),
|
||||||
operation: z.enum(['enable', 'disable', 'delete']),
|
documentIds: z
|
||||||
documentIds: z
|
.array(z.string())
|
||||||
.array(z.string())
|
.min(1, 'At least one document ID is required')
|
||||||
.min(1, 'At least one document ID is required')
|
.max(100, 'Cannot operate on more than 100 documents at once'),
|
||||||
.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 }> }) {
|
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
@@ -98,17 +90,14 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(req.url)
|
const url = new URL(req.url)
|
||||||
const enabledFilter = url.searchParams.get('enabledFilter') as
|
const includeDisabled = url.searchParams.get('includeDisabled') === 'true'
|
||||||
| 'all'
|
|
||||||
| 'enabled'
|
|
||||||
| 'disabled'
|
|
||||||
| null
|
|
||||||
const search = url.searchParams.get('search') || undefined
|
const search = url.searchParams.get('search') || undefined
|
||||||
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
|
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
|
||||||
const offset = Number.parseInt(url.searchParams.get('offset') || '0')
|
const offset = Number.parseInt(url.searchParams.get('offset') || '0')
|
||||||
const sortByParam = url.searchParams.get('sortBy')
|
const sortByParam = url.searchParams.get('sortBy')
|
||||||
const sortOrderParam = url.searchParams.get('sortOrder')
|
const sortOrderParam = url.searchParams.get('sortOrder')
|
||||||
|
|
||||||
|
// Validate sort parameters
|
||||||
const validSortFields: DocumentSortField[] = [
|
const validSortFields: DocumentSortField[] = [
|
||||||
'filename',
|
'filename',
|
||||||
'fileSize',
|
'fileSize',
|
||||||
@@ -116,7 +105,6 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
'chunkCount',
|
'chunkCount',
|
||||||
'uploadedAt',
|
'uploadedAt',
|
||||||
'processingStatus',
|
'processingStatus',
|
||||||
'enabled',
|
|
||||||
]
|
]
|
||||||
const validSortOrders: SortOrder[] = ['asc', 'desc']
|
const validSortOrders: SortOrder[] = ['asc', 'desc']
|
||||||
|
|
||||||
@@ -132,7 +120,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
const result = await getDocuments(
|
const result = await getDocuments(
|
||||||
knowledgeBaseId,
|
knowledgeBaseId,
|
||||||
{
|
{
|
||||||
enabledFilter: enabledFilter || undefined,
|
includeDisabled,
|
||||||
search,
|
search,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
@@ -202,7 +190,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
const createdDocuments = await createDocumentRecords(
|
const createdDocuments = await createDocumentRecords(
|
||||||
validatedData.documents,
|
validatedData.documents,
|
||||||
knowledgeBaseId,
|
knowledgeBaseId,
|
||||||
requestId
|
requestId,
|
||||||
|
userId
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -261,10 +250,16 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
throw validationError
|
throw validationError
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Handle single document creation
|
||||||
try {
|
try {
|
||||||
const validatedData = CreateDocumentSchema.parse(body)
|
const validatedData = CreateDocumentSchema.parse(body)
|
||||||
|
|
||||||
const newDocument = await createSingleDocument(validatedData, knowledgeBaseId, requestId)
|
const newDocument = await createSingleDocument(
|
||||||
|
validatedData,
|
||||||
|
knowledgeBaseId,
|
||||||
|
requestId,
|
||||||
|
userId
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { PlatformEvents } = await import('@/lib/core/telemetry')
|
const { PlatformEvents } = await import('@/lib/core/telemetry')
|
||||||
@@ -299,6 +294,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error creating document`, 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 errorMessage = error instanceof Error ? error.message : 'Failed to create document'
|
||||||
const isStorageLimitError =
|
const isStorageLimitError =
|
||||||
errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit')
|
errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit')
|
||||||
@@ -335,22 +331,16 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const validatedData = BulkUpdateDocumentsSchema.parse(body)
|
const validatedData = BulkUpdateDocumentsSchema.parse(body)
|
||||||
const { operation, documentIds, selectAll, enabledFilter } = validatedData
|
const { operation, documentIds } = validatedData
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result
|
const result = await bulkDocumentOperation(
|
||||||
if (selectAll) {
|
knowledgeBaseId,
|
||||||
result = await bulkDocumentOperationByFilter(
|
operation,
|
||||||
knowledgeBaseId,
|
documentIds,
|
||||||
operation,
|
requestId,
|
||||||
enabledFilter,
|
session.user.id
|
||||||
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -408,7 +408,6 @@ describe('Knowledge Search Utils', () => {
|
|||||||
input: ['test query'],
|
input: ['test query'],
|
||||||
model: 'text-embedding-3-small',
|
model: 'text-embedding-3-small',
|
||||||
encoding_format: 'float',
|
encoding_format: 'float',
|
||||||
dimensions: 1536,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||||
import { McpClient } from '@/lib/mcp/client'
|
import { McpClient } from '@/lib/mcp/client'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config'
|
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
|
||||||
import type { McpTransport } from '@/lib/mcp/types'
|
|
||||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||||
|
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
|
||||||
|
|
||||||
const logger = createLogger('McpServerTestAPI')
|
const logger = createLogger('McpServerTestAPI')
|
||||||
|
|
||||||
@@ -18,6 +19,30 @@ function isUrlBasedTransport(transport: McpTransport): boolean {
|
|||||||
return transport === 'streamable-http'
|
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 {
|
interface TestConnectionRequest {
|
||||||
name: string
|
name: string
|
||||||
transport: McpTransport
|
transport: McpTransport
|
||||||
@@ -71,30 +96,39 @@ export const POST = withMcpAuth('write')(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build initial config for resolution
|
let resolvedUrl = body.url
|
||||||
const initialConfig = {
|
let resolvedHeaders = body.headers || {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
|
||||||
|
|
||||||
|
if (resolvedUrl) {
|
||||||
|
resolvedUrl = resolveEnvVars(resolvedUrl, envVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedHeadersObj: Record<string, string> = {}
|
||||||
|
for (const [key, value] of Object.entries(resolvedHeaders)) {
|
||||||
|
resolvedHeadersObj[key] = resolveEnvVars(value, envVars)
|
||||||
|
}
|
||||||
|
resolvedHeaders = resolvedHeadersObj
|
||||||
|
} catch (envError) {
|
||||||
|
logger.warn(
|
||||||
|
`[${requestId}] Failed to resolve environment variables, using raw values:`,
|
||||||
|
envError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const testConfig: McpServerConfig = {
|
||||||
id: `test-${requestId}`,
|
id: `test-${requestId}`,
|
||||||
name: body.name,
|
name: body.name,
|
||||||
transport: body.transport,
|
transport: body.transport,
|
||||||
url: body.url,
|
url: resolvedUrl,
|
||||||
headers: body.headers || {},
|
headers: resolvedHeaders,
|
||||||
timeout: body.timeout || 10000,
|
timeout: body.timeout || 10000,
|
||||||
retries: 1, // Only one retry for tests
|
retries: 1, // Only one retry for tests
|
||||||
enabled: true,
|
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 = {
|
const testSecurityPolicy = {
|
||||||
requireConsent: false,
|
requireConsent: false,
|
||||||
auditLevel: 'none' as const,
|
auditLevel: 'none' as const,
|
||||||
|
|||||||
204
apps/sim/app/api/organizations/[id]/workspaces/route.ts
Normal file
204
apps/sim/app/api/organizations/[id]/workspaces/route.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { db } from '@sim/db'
|
||||||
|
import { member, permissions, user, workspace } from '@sim/db/schema'
|
||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { and, eq, or } from 'drizzle-orm'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
|
||||||
|
const logger = createLogger('OrganizationWorkspacesAPI')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/organizations/[id]/workspaces
|
||||||
|
* Get workspaces related to the organization with optional filtering
|
||||||
|
* Query parameters:
|
||||||
|
* - ?available=true - Only workspaces where user can invite others (admin permissions)
|
||||||
|
* - ?member=userId - Workspaces where specific member has access
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
try {
|
||||||
|
const session = await getSession()
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: organizationId } = await params
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const availableOnly = url.searchParams.get('available') === 'true'
|
||||||
|
const memberId = url.searchParams.get('member')
|
||||||
|
|
||||||
|
// Verify user is a member of this organization
|
||||||
|
const memberEntry = await db
|
||||||
|
.select()
|
||||||
|
.from(member)
|
||||||
|
.where(and(eq(member.organizationId, organizationId), eq(member.userId, session.user.id)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (memberEntry.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Forbidden - Not a member of this organization',
|
||||||
|
},
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRole = memberEntry[0].role
|
||||||
|
const hasAdminAccess = ['owner', 'admin'].includes(userRole)
|
||||||
|
|
||||||
|
if (availableOnly) {
|
||||||
|
// Get workspaces where user has admin permissions (can invite others)
|
||||||
|
const availableWorkspaces = await db
|
||||||
|
.select({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
ownerId: workspace.ownerId,
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
isOwner: eq(workspace.ownerId, session.user.id),
|
||||||
|
permissionType: permissions.permissionType,
|
||||||
|
})
|
||||||
|
.from(workspace)
|
||||||
|
.leftJoin(
|
||||||
|
permissions,
|
||||||
|
and(
|
||||||
|
eq(permissions.entityType, 'workspace'),
|
||||||
|
eq(permissions.entityId, workspace.id),
|
||||||
|
eq(permissions.userId, session.user.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
// User owns the workspace
|
||||||
|
eq(workspace.ownerId, session.user.id),
|
||||||
|
// User has admin permission on the workspace
|
||||||
|
and(
|
||||||
|
eq(permissions.userId, session.user.id),
|
||||||
|
eq(permissions.entityType, 'workspace'),
|
||||||
|
eq(permissions.permissionType, 'admin')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter and format the results
|
||||||
|
const workspacesWithInvitePermission = availableWorkspaces
|
||||||
|
.filter((workspace) => {
|
||||||
|
// Include if user owns the workspace OR has admin permission
|
||||||
|
return workspace.isOwner || workspace.permissionType === 'admin'
|
||||||
|
})
|
||||||
|
.map((workspace) => ({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
isOwner: workspace.isOwner,
|
||||||
|
canInvite: true, // All returned workspaces have invite permission
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
}))
|
||||||
|
|
||||||
|
logger.info('Retrieved available workspaces for organization member', {
|
||||||
|
organizationId,
|
||||||
|
userId: session.user.id,
|
||||||
|
workspaceCount: workspacesWithInvitePermission.length,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
workspaces: workspacesWithInvitePermission,
|
||||||
|
totalCount: workspacesWithInvitePermission.length,
|
||||||
|
filter: 'available',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberId && hasAdminAccess) {
|
||||||
|
// Get workspaces where specific member has access (admin only)
|
||||||
|
const memberWorkspaces = await db
|
||||||
|
.select({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
ownerId: workspace.ownerId,
|
||||||
|
isOwner: eq(workspace.ownerId, memberId),
|
||||||
|
permissionType: permissions.permissionType,
|
||||||
|
createdAt: permissions.createdAt,
|
||||||
|
})
|
||||||
|
.from(workspace)
|
||||||
|
.leftJoin(
|
||||||
|
permissions,
|
||||||
|
and(
|
||||||
|
eq(permissions.entityType, 'workspace'),
|
||||||
|
eq(permissions.entityId, workspace.id),
|
||||||
|
eq(permissions.userId, memberId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
// Member owns the workspace
|
||||||
|
eq(workspace.ownerId, memberId),
|
||||||
|
// Member has permissions on the workspace
|
||||||
|
and(eq(permissions.userId, memberId), eq(permissions.entityType, 'workspace'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const formattedWorkspaces = memberWorkspaces.map((workspace) => ({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
isOwner: workspace.isOwner,
|
||||||
|
permission: workspace.permissionType,
|
||||||
|
joinedAt: workspace.createdAt,
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
workspaces: formattedWorkspaces,
|
||||||
|
totalCount: formattedWorkspaces.length,
|
||||||
|
filter: 'member',
|
||||||
|
memberId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: Get all workspaces (basic info only for regular members)
|
||||||
|
if (!hasAdminAccess) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
workspaces: [],
|
||||||
|
totalCount: 0,
|
||||||
|
message: 'Workspace access information is only available to organization admins',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// For admins: Get summary of all workspaces
|
||||||
|
const allWorkspaces = await db
|
||||||
|
.select({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
ownerId: workspace.ownerId,
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
ownerName: user.name,
|
||||||
|
})
|
||||||
|
.from(workspace)
|
||||||
|
.leftJoin(user, eq(workspace.ownerId, user.id))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
workspaces: allWorkspaces,
|
||||||
|
totalCount: allWorkspaces.length,
|
||||||
|
filter: 'all',
|
||||||
|
},
|
||||||
|
userRole,
|
||||||
|
hasAdminAccess,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get organization workspaces', { error })
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Internal server error',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,7 @@ import { account } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
|
||||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
import type { StreamingExecution } from '@/executor/types'
|
import type { StreamingExecution } from '@/executor/types'
|
||||||
import { executeProviderRequest } from '@/providers'
|
import { executeProviderRequest } from '@/providers'
|
||||||
@@ -22,11 +20,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
try {
|
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`, {
|
logger.info(`[${requestId}] Provider API request started`, {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
userAgent: request.headers.get('User-Agent'),
|
userAgent: request.headers.get('User-Agent'),
|
||||||
@@ -92,13 +85,6 @@ export async function POST(request: NextRequest) {
|
|||||||
verbosity,
|
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
|
let finalApiKey: string | undefined = apiKey
|
||||||
try {
|
try {
|
||||||
if (provider === 'vertex' && vertexCredential) {
|
if (provider === 'vertex' && vertexCredential) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateExternalUrl } from '@/lib/core/security/input-validation'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -40,18 +39,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const validatedData = A2ASetPushNotificationSchema.parse(body)
|
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`, {
|
logger.info(`[${requestId}] A2A set push notification request`, {
|
||||||
agentUrl: validatedData.agentUrl,
|
agentUrl: validatedData.agentUrl,
|
||||||
taskId: validatedData.taskId,
|
taskId: validatedData.taskId,
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'user-123',
|
userId: 'user-123',
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
@@ -254,7 +254,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: 'Unauthorized',
|
||||||
}),
|
}),
|
||||||
@@ -304,7 +304,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
describe('POST /api/tools/custom', () => {
|
describe('POST /api/tools/custom', () => {
|
||||||
it('should reject unauthorized requests', async () => {
|
it('should reject unauthorized requests', async () => {
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: 'Unauthorized',
|
||||||
}),
|
}),
|
||||||
@@ -390,7 +390,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
|
|
||||||
it('should prevent unauthorized deletion of user-scoped tool', async () => {
|
it('should prevent unauthorized deletion of user-scoped tool', async () => {
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'user-456',
|
userId: 'user-456',
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
@@ -413,7 +413,7 @@ describe('Custom Tools API Routes', () => {
|
|||||||
|
|
||||||
it('should reject unauthorized requests', async () => {
|
it('should reject unauthorized requests', async () => {
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: 'Unauthorized',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, desc, eq, isNull, or } from 'drizzle-orm'
|
import { and, desc, eq, isNull, or } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -42,8 +42,8 @@ export async function GET(request: NextRequest) {
|
|||||||
const workflowId = searchParams.get('workflowId')
|
const workflowId = searchParams.get('workflowId')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use session/internal auth to support session and internal JWT (no API key access)
|
// Use hybrid auth to support session, API key, and internal JWT
|
||||||
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized custom tools access attempt`)
|
logger.warn(`[${requestId}] Unauthorized custom tools access attempt`)
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
@@ -69,8 +69,8 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check workspace permissions
|
// Check workspace permissions
|
||||||
// For internal JWT with workflowId: checkSessionOrInternalAuth already resolved userId from workflow owner
|
// For internal JWT with workflowId: checkHybridAuth already resolved userId from workflow owner
|
||||||
// For session: verify user has access to the workspace
|
// For session/API key: verify user has access to the workspace
|
||||||
// For legacy (no workspaceId): skip workspace check, rely on userId match
|
// For legacy (no workspaceId): skip workspace check, rely on userId match
|
||||||
if (resolvedWorkspaceId && !(authResult.authType === 'internal_jwt' && workflowId)) {
|
if (resolvedWorkspaceId && !(authResult.authType === 'internal_jwt' && workflowId)) {
|
||||||
const userPermission = await getUserEntityPermissions(
|
const userPermission = await getUserEntityPermissions(
|
||||||
@@ -116,8 +116,8 @@ export async function POST(req: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use session/internal auth (no API key access)
|
// Use hybrid auth (though this endpoint is only called from UI)
|
||||||
const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(req, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized custom tools update attempt`)
|
logger.warn(`[${requestId}] Unauthorized custom tools update attempt`)
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
@@ -193,8 +193,8 @@ export async function DELETE(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use session/internal auth (no API key access)
|
// Use hybrid auth (though this endpoint is only called from UI)
|
||||||
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized custom tool deletion attempt`)
|
logger.warn(`[${requestId}] Unauthorized custom tool deletion attempt`)
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateNumericId } from '@/lib/core/security/input-validation'
|
import { validateNumericId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail add label attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail add label attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail archive attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail archive attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail delete attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail delete attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail mark read attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail mark read attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail mark unread attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail mark unread attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail move attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail move attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail remove label attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail remove label attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Gmail unarchive attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Gmail unarchive attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateImageUrl } from '@/lib/core/security/input-validation'
|
import { validateImageUrl } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const imageUrl = url.searchParams.get('url')
|
const imageUrl = url.searchParams.get('url')
|
||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.error(`[${requestId}] Authentication failed for image proxy:`, authResult.error)
|
logger.error(`[${requestId}] Authentication failed for image proxy:`, authResult.error)
|
||||||
return new NextResponse('Unauthorized', { status: 401 })
|
return new NextResponse('Unauthorized', { status: 401 })
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { Resend } from 'resend'
|
import { Resend } from 'resend'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized mail send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized mail send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Teams chat delete attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Teams chat delete attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { StorageService } from '@/lib/uploads'
|
import { StorageService } from '@/lib/uploads'
|
||||||
@@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Mistral parse attempt`, {
|
logger.warn(`[${requestId}] Unauthorized Mistral parse attempt`, {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLDeleteAPI')
|
const logger = createLogger('MySQLDeleteAPI')
|
||||||
@@ -22,12 +21,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = DeleteSchema.parse(body)
|
const params = DeleteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLExecuteAPI')
|
const logger = createLogger('MySQLExecuteAPI')
|
||||||
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = ExecuteSchema.parse(body)
|
const params = ExecuteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLInsertAPI')
|
const logger = createLogger('MySQLInsertAPI')
|
||||||
@@ -43,12 +42,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = InsertSchema.parse(body)
|
const params = InsertSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'
|
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLIntrospectAPI')
|
const logger = createLogger('MySQLIntrospectAPI')
|
||||||
@@ -20,12 +19,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = IntrospectSchema.parse(body)
|
const params = IntrospectSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLQueryAPI')
|
const logger = createLogger('MySQLQueryAPI')
|
||||||
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = QuerySchema.parse(body)
|
const params = QuerySchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
|
||||||
|
|
||||||
const logger = createLogger('MySQLUpdateAPI')
|
const logger = createLogger('MySQLUpdateAPI')
|
||||||
@@ -41,12 +40,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = UpdateSchema.parse(body)
|
const params = UpdateSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import {
|
import {
|
||||||
@@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook copy attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook copy attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook delete attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook delete attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook mark read attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook mark read attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook mark unread attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook mark unread attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook move attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook move attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createPostgresConnection, executeDelete } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeDelete } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLDeleteAPI')
|
const logger = createLogger('PostgreSQLDeleteAPI')
|
||||||
@@ -22,12 +21,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = DeleteSchema.parse(body)
|
const params = DeleteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
createPostgresConnection,
|
createPostgresConnection,
|
||||||
executeQuery,
|
executeQuery,
|
||||||
@@ -25,12 +24,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = ExecuteSchema.parse(body)
|
const params = ExecuteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createPostgresConnection, executeInsert } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeInsert } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLInsertAPI')
|
const logger = createLogger('PostgreSQLInsertAPI')
|
||||||
@@ -43,12 +42,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
|
|
||||||
const params = InsertSchema.parse(body)
|
const params = InsertSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createPostgresConnection, executeIntrospect } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeIntrospect } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLIntrospectAPI')
|
const logger = createLogger('PostgreSQLIntrospectAPI')
|
||||||
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = IntrospectSchema.parse(body)
|
const params = IntrospectSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createPostgresConnection, executeQuery } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeQuery } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLQueryAPI')
|
const logger = createLogger('PostgreSQLQueryAPI')
|
||||||
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = QuerySchema.parse(body)
|
const params = QuerySchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createPostgresConnection, executeUpdate } from '@/app/api/tools/postgresql/utils'
|
import { createPostgresConnection, executeUpdate } from '@/app/api/tools/postgresql/utils'
|
||||||
|
|
||||||
const logger = createLogger('PostgreSQLUpdateAPI')
|
const logger = createLogger('PostgreSQLUpdateAPI')
|
||||||
@@ -41,12 +40,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = UpdateSchema.parse(body)
|
const params = UpdateSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { StorageService } from '@/lib/uploads'
|
import { StorageService } from '@/lib/uploads'
|
||||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Pulse parse attempt`, {
|
logger.warn(`[${requestId}] Unauthorized Pulse parse attempt`, {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { StorageService } from '@/lib/uploads'
|
import { StorageService } from '@/lib/uploads'
|
||||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized Reducto parse attempt`, {
|
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 { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized S3 copy object attempt: ${authResult.error}`)
|
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 { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized S3 delete object attempt: ${authResult.error}`)
|
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 { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized S3 list objects attempt: ${authResult.error}`)
|
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 { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { SEARCH_TOOL_COST } from '@/lib/billing/constants'
|
import { SEARCH_TOOL_COST } from '@/lib/billing/constants'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { executeTool } from '@/tools'
|
import { executeTool } from '@/tools'
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const { searchParams: urlParams } = new URL(request.url)
|
const { searchParams: urlParams } = new URL(request.url)
|
||||||
const workflowId = urlParams.get('workflowId') || undefined
|
const workflowId = urlParams.get('workflowId') || undefined
|
||||||
|
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
const errorMessage = workflowId ? 'Workflow not found' : authResult.error || 'Unauthorized'
|
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 NextRequest, NextResponse } from 'next/server'
|
||||||
import type { SFTPWrapper } from 'ssh2'
|
import type { SFTPWrapper } from 'ssh2'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import {
|
import {
|
||||||
createSftpConnection,
|
createSftpConnection,
|
||||||
@@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SFTP delete attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SFTP delete attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from 'path'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils'
|
import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils'
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SFTP download attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SFTP download attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import {
|
import {
|
||||||
createSftpConnection,
|
createSftpConnection,
|
||||||
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized SFTP list attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized SFTP list attempt: ${authResult.error}`)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user