mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-05 04:05:14 -05:00
Compare commits
37 Commits
v0.5.78
...
fix/onedri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60053a0dd7 | ||
|
|
5f719b3746 | ||
|
|
5fde0bd941 | ||
|
|
9e22d4aaa9 | ||
|
|
994224322a | ||
|
|
a02102345e | ||
|
|
e256261d53 | ||
|
|
552dc56fc3 | ||
|
|
2147309365 | ||
|
|
36ec68d93e | ||
|
|
fce566cc2f | ||
|
|
1933e1aad5 | ||
|
|
793adda986 | ||
|
|
8d846c5983 | ||
|
|
362f4c2918 | ||
|
|
c77e351067 | ||
|
|
a627faabe7 | ||
|
|
f811594875 | ||
|
|
0bc245b7a9 | ||
|
|
0a08ac03b9 | ||
|
|
7977ac88ca | ||
|
|
5b0c2156e0 | ||
|
|
4db6e556b7 | ||
|
|
4ba22527b6 | ||
|
|
c51f266ad7 | ||
|
|
4ca00810b2 | ||
|
|
710bf75bca | ||
|
|
f21fe2309c | ||
|
|
9c3fd1f7af | ||
|
|
a9b7d75d87 | ||
|
|
0449804ffb | ||
|
|
c286f3ed24 | ||
|
|
b738550815 | ||
|
|
c6357f7438 | ||
|
|
b1118935f7 | ||
|
|
3e18b4186c | ||
|
|
e1ac201936 |
@@ -183,6 +183,109 @@ export const {ServiceName}Block: BlockConfig = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## File Input Handling
|
||||||
|
|
||||||
|
When your block accepts file uploads, use the basic/advanced mode pattern with `normalizeFileInput`.
|
||||||
|
|
||||||
|
### Basic/Advanced File Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic mode: Visual file upload
|
||||||
|
{
|
||||||
|
id: 'uploadFile',
|
||||||
|
title: 'File',
|
||||||
|
type: 'file-upload',
|
||||||
|
canonicalParamId: 'file', // Both map to 'file' param
|
||||||
|
placeholder: 'Upload file',
|
||||||
|
mode: 'basic',
|
||||||
|
multiple: false,
|
||||||
|
required: true,
|
||||||
|
condition: { field: 'operation', value: 'upload' },
|
||||||
|
},
|
||||||
|
// Advanced mode: Reference from other blocks
|
||||||
|
{
|
||||||
|
id: 'fileRef',
|
||||||
|
title: 'File',
|
||||||
|
type: 'short-input',
|
||||||
|
canonicalParamId: 'file', // Both map to 'file' param
|
||||||
|
placeholder: 'Reference file (e.g., {{file_block.output}})',
|
||||||
|
mode: 'advanced',
|
||||||
|
required: true,
|
||||||
|
condition: { field: 'operation', value: 'upload' },
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical constraints:**
|
||||||
|
- `canonicalParamId` must NOT match any subblock's `id` in the same block
|
||||||
|
- Values are stored under subblock `id`, not `canonicalParamId`
|
||||||
|
|
||||||
|
### Normalizing File Input in tools.config
|
||||||
|
|
||||||
|
Use `normalizeFileInput` to handle all input variants:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { normalizeFileInput } from '@/blocks/utils'
|
||||||
|
|
||||||
|
tools: {
|
||||||
|
access: ['service_upload'],
|
||||||
|
config: {
|
||||||
|
tool: (params) => {
|
||||||
|
// Check all field IDs: uploadFile (basic), fileRef (advanced), fileContent (legacy)
|
||||||
|
const normalizedFile = normalizeFileInput(
|
||||||
|
params.uploadFile || params.fileRef || params.fileContent,
|
||||||
|
{ single: true }
|
||||||
|
)
|
||||||
|
if (normalizedFile) {
|
||||||
|
params.file = normalizedFile
|
||||||
|
}
|
||||||
|
return `service_${params.operation}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this pattern?**
|
||||||
|
- Values come through as `params.uploadFile` or `params.fileRef` (the subblock IDs)
|
||||||
|
- `canonicalParamId` only controls UI/schema mapping, not runtime values
|
||||||
|
- `normalizeFileInput` handles JSON strings from advanced mode template resolution
|
||||||
|
|
||||||
|
### File Input Types in `inputs`
|
||||||
|
|
||||||
|
Use `type: 'json'` for file inputs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
inputs: {
|
||||||
|
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
|
||||||
|
fileRef: { type: 'json', description: 'File reference from previous block' },
|
||||||
|
// Legacy field for backwards compatibility
|
||||||
|
fileContent: { type: 'string', description: 'Legacy: base64 encoded content' },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Files
|
||||||
|
|
||||||
|
For multiple file uploads:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: 'attachments',
|
||||||
|
title: 'Attachments',
|
||||||
|
type: 'file-upload',
|
||||||
|
multiple: true, // Allow multiple files
|
||||||
|
maxSize: 25, // Max size in MB per file
|
||||||
|
acceptedTypes: 'image/*,application/pdf,.doc,.docx',
|
||||||
|
}
|
||||||
|
|
||||||
|
// In tools.config:
|
||||||
|
const normalizedFiles = normalizeFileInput(
|
||||||
|
params.attachments || params.attachmentRefs,
|
||||||
|
// No { single: true } - returns array
|
||||||
|
)
|
||||||
|
if (normalizedFiles) {
|
||||||
|
params.files = normalizedFiles
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Condition Syntax
|
## Condition Syntax
|
||||||
|
|
||||||
Controls when a field is shown based on other field values.
|
Controls when a field is shown based on other field values.
|
||||||
|
|||||||
@@ -206,10 +206,15 @@ export const {Service}Block: BlockConfig = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Critical:**
|
**Critical Canonical Param Rules:**
|
||||||
- `canonicalParamId` must NOT match any other subblock's `id`, must be unique per block, and should only be used to link basic/advanced alternatives for the same parameter.
|
- `canonicalParamId` must NOT match any subblock's `id` in the block
|
||||||
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent.
|
- `canonicalParamId` must be unique per operation/condition context
|
||||||
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
|
- Only use `canonicalParamId` to link basic/advanced alternatives for the same logical parameter
|
||||||
|
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent
|
||||||
|
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions
|
||||||
|
- **Required consistency:** If one subblock in a canonical group has `required: true`, ALL subblocks in that group must have `required: true` (prevents bypassing validation by switching modes)
|
||||||
|
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs (e.g., `fileSelector`, `manualFileId`)
|
||||||
|
- **Params function:** Must use canonical param IDs, NOT raw subblock IDs (raw IDs are deleted after canonical transformation)
|
||||||
|
|
||||||
## Step 4: Add Icon
|
## Step 4: Add Icon
|
||||||
|
|
||||||
@@ -457,7 +462,230 @@ You can usually find this in the service's brand/press kit page, or copy it from
|
|||||||
Paste the SVG code here and I'll convert it to a React component.
|
Paste the SVG code here and I'll convert it to a React component.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Gotchas
|
## File Handling
|
||||||
|
|
||||||
|
When your integration handles file uploads or downloads, follow these patterns to work with `UserFile` objects consistently.
|
||||||
|
|
||||||
|
### What is a UserFile?
|
||||||
|
|
||||||
|
A `UserFile` is the standard file representation in Sim:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UserFile {
|
||||||
|
id: string // Unique identifier
|
||||||
|
name: string // Original filename
|
||||||
|
url: string // Presigned URL for download
|
||||||
|
size: number // File size in bytes
|
||||||
|
type: string // MIME type (e.g., 'application/pdf')
|
||||||
|
base64?: string // Optional base64 content (if small file)
|
||||||
|
key?: string // Internal storage key
|
||||||
|
context?: object // Storage context metadata
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Input Pattern (Uploads)
|
||||||
|
|
||||||
|
For tools that accept file uploads, **always route through an internal API endpoint** rather than calling external APIs directly. This ensures proper file content retrieval.
|
||||||
|
|
||||||
|
#### 1. Block SubBlocks for File Input
|
||||||
|
|
||||||
|
Use the basic/advanced mode pattern:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic mode: File upload UI
|
||||||
|
{
|
||||||
|
id: 'uploadFile',
|
||||||
|
title: 'File',
|
||||||
|
type: 'file-upload',
|
||||||
|
canonicalParamId: 'file', // Maps to 'file' param
|
||||||
|
placeholder: 'Upload file',
|
||||||
|
mode: 'basic',
|
||||||
|
multiple: false,
|
||||||
|
required: true,
|
||||||
|
condition: { field: 'operation', value: 'upload' },
|
||||||
|
},
|
||||||
|
// Advanced mode: Reference from previous block
|
||||||
|
{
|
||||||
|
id: 'fileRef',
|
||||||
|
title: 'File',
|
||||||
|
type: 'short-input',
|
||||||
|
canonicalParamId: 'file', // Same canonical param
|
||||||
|
placeholder: 'Reference file (e.g., {{file_block.output}})',
|
||||||
|
mode: 'advanced',
|
||||||
|
required: true,
|
||||||
|
condition: { field: 'operation', value: 'upload' },
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical:** `canonicalParamId` must NOT match any subblock `id`.
|
||||||
|
|
||||||
|
#### 2. Normalize File Input in Block Config
|
||||||
|
|
||||||
|
In `tools.config.tool`, use `normalizeFileInput` to handle all input variants:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { normalizeFileInput } from '@/blocks/utils'
|
||||||
|
|
||||||
|
tools: {
|
||||||
|
config: {
|
||||||
|
tool: (params) => {
|
||||||
|
// Normalize file from basic (uploadFile), advanced (fileRef), or legacy (fileContent)
|
||||||
|
const normalizedFile = normalizeFileInput(
|
||||||
|
params.uploadFile || params.fileRef || params.fileContent,
|
||||||
|
{ single: true }
|
||||||
|
)
|
||||||
|
if (normalizedFile) {
|
||||||
|
params.file = normalizedFile
|
||||||
|
}
|
||||||
|
return `{service}_${params.operation}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Create Internal API Route
|
||||||
|
|
||||||
|
Create `apps/sim/app/api/tools/{service}/{action}/route.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { NextResponse, type NextRequest } from 'next/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { FileInputSchema, type RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||||
|
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||||
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
|
|
||||||
|
const logger = createLogger('{Service}UploadAPI')
|
||||||
|
|
||||||
|
const RequestSchema = z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
file: FileInputSchema.optional().nullable(),
|
||||||
|
// Legacy field for backwards compatibility
|
||||||
|
fileContent: z.string().optional().nullable(),
|
||||||
|
// ... other params
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
if (!authResult.success) {
|
||||||
|
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const data = RequestSchema.parse(body)
|
||||||
|
|
||||||
|
let fileBuffer: Buffer
|
||||||
|
let fileName: string
|
||||||
|
|
||||||
|
// Prefer UserFile input, fall back to legacy base64
|
||||||
|
if (data.file) {
|
||||||
|
const userFiles = processFilesToUserFiles([data.file as RawFileInput], requestId, logger)
|
||||||
|
if (userFiles.length === 0) {
|
||||||
|
return NextResponse.json({ success: false, error: 'Invalid file' }, { status: 400 })
|
||||||
|
}
|
||||||
|
const userFile = userFiles[0]
|
||||||
|
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||||
|
fileName = userFile.name
|
||||||
|
} else if (data.fileContent) {
|
||||||
|
// Legacy: base64 string (backwards compatibility)
|
||||||
|
fileBuffer = Buffer.from(data.fileContent, 'base64')
|
||||||
|
fileName = 'file'
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ success: false, error: 'File required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now call external API with fileBuffer
|
||||||
|
const response = await fetch('https://api.{service}.com/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${data.accessToken}` },
|
||||||
|
body: new Uint8Array(fileBuffer), // Convert Buffer for fetch
|
||||||
|
})
|
||||||
|
|
||||||
|
// ... handle response
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Update Tool to Use Internal Route
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const {service}UploadTool: ToolConfig<Params, Response> = {
|
||||||
|
id: '{service}_upload',
|
||||||
|
// ...
|
||||||
|
params: {
|
||||||
|
file: { type: 'file', required: false, visibility: 'user-or-llm' },
|
||||||
|
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
url: '/api/tools/{service}/upload', // Internal route
|
||||||
|
method: 'POST',
|
||||||
|
body: (params) => ({
|
||||||
|
accessToken: params.accessToken,
|
||||||
|
file: params.file,
|
||||||
|
fileContent: params.fileContent,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Output Pattern (Downloads)
|
||||||
|
|
||||||
|
For tools that return files, use `FileToolProcessor` to store files and return `UserFile` objects.
|
||||||
|
|
||||||
|
#### In Tool transformResponse
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
|
||||||
|
|
||||||
|
transformResponse: async (response, context) => {
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// Process file outputs to UserFile objects
|
||||||
|
const fileProcessor = new FileToolProcessor(context)
|
||||||
|
const file = await fileProcessor.processFileData({
|
||||||
|
data: data.content, // base64 or buffer
|
||||||
|
mimeType: data.mimeType,
|
||||||
|
filename: data.filename,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: { file },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In API Route (for complex file handling)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Return file data that FileToolProcessor can handle
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
output: {
|
||||||
|
file: {
|
||||||
|
data: base64Content,
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
filename: 'document.pdf',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Helpers Reference
|
||||||
|
|
||||||
|
| Helper | Location | Purpose |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| `normalizeFileInput` | `@/blocks/utils` | Normalize file params in block config |
|
||||||
|
| `processFilesToUserFiles` | `@/lib/uploads/utils/file-utils` | Convert raw inputs to UserFile[] |
|
||||||
|
| `downloadFileFromStorage` | `@/lib/uploads/utils/file-utils.server` | Get file Buffer from UserFile |
|
||||||
|
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
|
||||||
|
| `isUserFile` | `@/lib/core/utils/user-file` | Type guard for UserFile objects |
|
||||||
|
| `FileInputSchema` | `@/lib/uploads/utils/file-schemas` | Zod schema for file validation |
|
||||||
|
|
||||||
|
### Common Gotchas
|
||||||
|
|
||||||
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
|
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
|
||||||
2. **Tool IDs are snake_case** - `stripe_create_payment`, not `stripeCreatePayment`
|
2. **Tool IDs are snake_case** - `stripe_create_payment`, not `stripeCreatePayment`
|
||||||
@@ -465,3 +693,5 @@ Paste the SVG code here and I'll convert it to a React component.
|
|||||||
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
|
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
|
||||||
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
|
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
|
||||||
6. **DependsOn clears options** - When a dependency changes, selector options are refetched
|
6. **DependsOn clears options** - When a dependency changes, selector options are refetched
|
||||||
|
7. **Never pass Buffer directly to fetch** - Convert to `new Uint8Array(buffer)` for TypeScript compatibility
|
||||||
|
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility
|
||||||
|
|||||||
@@ -157,6 +157,36 @@ dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
|
|||||||
- `'both'` - Show in both modes (default)
|
- `'both'` - Show in both modes (default)
|
||||||
- `'trigger'` - Only when block is used as trigger
|
- `'trigger'` - Only when block is used as trigger
|
||||||
|
|
||||||
|
### `canonicalParamId` - Link basic/advanced alternatives
|
||||||
|
|
||||||
|
Use to map multiple UI inputs to a single logical parameter:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic mode: Visual selector
|
||||||
|
{
|
||||||
|
id: 'fileSelector',
|
||||||
|
type: 'file-selector',
|
||||||
|
mode: 'basic',
|
||||||
|
canonicalParamId: 'fileId',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// Advanced mode: Manual input
|
||||||
|
{
|
||||||
|
id: 'manualFileId',
|
||||||
|
type: 'short-input',
|
||||||
|
mode: 'advanced',
|
||||||
|
canonicalParamId: 'fileId',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical Rules:**
|
||||||
|
- `canonicalParamId` must NOT match any subblock's `id`
|
||||||
|
- `canonicalParamId` must be unique per operation/condition context
|
||||||
|
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
|
||||||
|
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
|
||||||
|
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
|
||||||
|
|
||||||
**Register in `blocks/registry.ts`:**
|
**Register in `blocks/registry.ts`:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -195,6 +225,52 @@ import { {service}WebhookTrigger } from '@/triggers/{service}'
|
|||||||
{service}_webhook: {service}WebhookTrigger,
|
{service}_webhook: {service}WebhookTrigger,
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## File Handling
|
||||||
|
|
||||||
|
When integrations handle file uploads/downloads, use `UserFile` objects consistently.
|
||||||
|
|
||||||
|
### File Input (Uploads)
|
||||||
|
|
||||||
|
1. **Block subBlocks:** Use basic/advanced mode pattern with `canonicalParamId`
|
||||||
|
2. **Normalize in block config:** Use `normalizeFileInput` from `@/blocks/utils`
|
||||||
|
3. **Internal API route:** Create route that uses `downloadFileFromStorage` to get file content
|
||||||
|
4. **Tool routes to internal API:** Don't call external APIs directly with files
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In block tools.config:
|
||||||
|
import { normalizeFileInput } from '@/blocks/utils'
|
||||||
|
|
||||||
|
const normalizedFile = normalizeFileInput(
|
||||||
|
params.uploadFile || params.fileRef || params.fileContent,
|
||||||
|
{ single: true }
|
||||||
|
)
|
||||||
|
if (normalizedFile) params.file = normalizedFile
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Output (Downloads)
|
||||||
|
|
||||||
|
Use `FileToolProcessor` in tool `transformResponse` to store files:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
|
||||||
|
|
||||||
|
const processor = new FileToolProcessor(context)
|
||||||
|
const file = await processor.processFileData({
|
||||||
|
data: base64Content,
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
filename: 'doc.pdf',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Helpers
|
||||||
|
|
||||||
|
| Helper | Location | Purpose |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| `normalizeFileInput` | `@/blocks/utils` | Normalize file params in block config |
|
||||||
|
| `processFilesToUserFiles` | `@/lib/uploads/utils/file-utils` | Convert raw inputs to UserFile[] |
|
||||||
|
| `downloadFileFromStorage` | `@/lib/uploads/utils/file-utils.server` | Get Buffer from UserFile |
|
||||||
|
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Look up API docs for the service
|
- [ ] Look up API docs for the service
|
||||||
@@ -207,3 +283,5 @@ import { {service}WebhookTrigger } from '@/triggers/{service}'
|
|||||||
- [ ] Register block in `blocks/registry.ts`
|
- [ ] Register block in `blocks/registry.ts`
|
||||||
- [ ] (Optional) Create triggers in `triggers/{service}/`
|
- [ ] (Optional) Create triggers in `triggers/{service}/`
|
||||||
- [ ] (Optional) Register triggers in `triggers/registry.ts`
|
- [ ] (Optional) Register triggers in `triggers/registry.ts`
|
||||||
|
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
|
||||||
|
- [ ] (If file uploads) Use `normalizeFileInput` in block config
|
||||||
|
|||||||
@@ -155,6 +155,36 @@ dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
|
|||||||
- `'both'` - Show in both modes (default)
|
- `'both'` - Show in both modes (default)
|
||||||
- `'trigger'` - Only when block is used as trigger
|
- `'trigger'` - Only when block is used as trigger
|
||||||
|
|
||||||
|
### `canonicalParamId` - Link basic/advanced alternatives
|
||||||
|
|
||||||
|
Use to map multiple UI inputs to a single logical parameter:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic mode: Visual selector
|
||||||
|
{
|
||||||
|
id: 'fileSelector',
|
||||||
|
type: 'file-selector',
|
||||||
|
mode: 'basic',
|
||||||
|
canonicalParamId: 'fileId',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// Advanced mode: Manual input
|
||||||
|
{
|
||||||
|
id: 'manualFileId',
|
||||||
|
type: 'short-input',
|
||||||
|
mode: 'advanced',
|
||||||
|
canonicalParamId: 'fileId',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical Rules:**
|
||||||
|
- `canonicalParamId` must NOT match any subblock's `id`
|
||||||
|
- `canonicalParamId` must be unique per operation/condition context
|
||||||
|
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
|
||||||
|
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
|
||||||
|
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
|
||||||
|
|
||||||
**Register in `blocks/registry.ts`:**
|
**Register in `blocks/registry.ts`:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -193,6 +223,52 @@ import { {service}WebhookTrigger } from '@/triggers/{service}'
|
|||||||
{service}_webhook: {service}WebhookTrigger,
|
{service}_webhook: {service}WebhookTrigger,
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## File Handling
|
||||||
|
|
||||||
|
When integrations handle file uploads/downloads, use `UserFile` objects consistently.
|
||||||
|
|
||||||
|
### File Input (Uploads)
|
||||||
|
|
||||||
|
1. **Block subBlocks:** Use basic/advanced mode pattern with `canonicalParamId`
|
||||||
|
2. **Normalize in block config:** Use `normalizeFileInput` from `@/blocks/utils`
|
||||||
|
3. **Internal API route:** Create route that uses `downloadFileFromStorage` to get file content
|
||||||
|
4. **Tool routes to internal API:** Don't call external APIs directly with files
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In block tools.config:
|
||||||
|
import { normalizeFileInput } from '@/blocks/utils'
|
||||||
|
|
||||||
|
const normalizedFile = normalizeFileInput(
|
||||||
|
params.uploadFile || params.fileRef || params.fileContent,
|
||||||
|
{ single: true }
|
||||||
|
)
|
||||||
|
if (normalizedFile) params.file = normalizedFile
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Output (Downloads)
|
||||||
|
|
||||||
|
Use `FileToolProcessor` in tool `transformResponse` to store files:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
|
||||||
|
|
||||||
|
const processor = new FileToolProcessor(context)
|
||||||
|
const file = await processor.processFileData({
|
||||||
|
data: base64Content,
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
filename: 'doc.pdf',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Helpers
|
||||||
|
|
||||||
|
| Helper | Location | Purpose |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| `normalizeFileInput` | `@/blocks/utils` | Normalize file params in block config |
|
||||||
|
| `processFilesToUserFiles` | `@/lib/uploads/utils/file-utils` | Convert raw inputs to UserFile[] |
|
||||||
|
| `downloadFileFromStorage` | `@/lib/uploads/utils/file-utils.server` | Get Buffer from UserFile |
|
||||||
|
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Look up API docs for the service
|
- [ ] Look up API docs for the service
|
||||||
@@ -205,3 +281,5 @@ import { {service}WebhookTrigger } from '@/triggers/{service}'
|
|||||||
- [ ] Register block in `blocks/registry.ts`
|
- [ ] Register block in `blocks/registry.ts`
|
||||||
- [ ] (Optional) Create triggers in `triggers/{service}/`
|
- [ ] (Optional) Create triggers in `triggers/{service}/`
|
||||||
- [ ] (Optional) Register triggers in `triggers/registry.ts`
|
- [ ] (Optional) Register triggers in `triggers/registry.ts`
|
||||||
|
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
|
||||||
|
- [ ] (If file uploads) Use `normalizeFileInput` in block config
|
||||||
|
|||||||
19
CLAUDE.md
19
CLAUDE.md
@@ -265,6 +265,23 @@ Register in `blocks/registry.ts` (alphabetically).
|
|||||||
|
|
||||||
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
|
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
|
||||||
|
|
||||||
|
**File Input Pattern (basic/advanced mode):**
|
||||||
|
```typescript
|
||||||
|
// Basic: file-upload UI
|
||||||
|
{ id: 'uploadFile', type: 'file-upload', canonicalParamId: 'file', mode: 'basic' },
|
||||||
|
// Advanced: reference from other blocks
|
||||||
|
{ id: 'fileRef', type: 'short-input', canonicalParamId: 'file', mode: 'advanced' },
|
||||||
|
```
|
||||||
|
|
||||||
|
In `tools.config.tool`, normalize with:
|
||||||
|
```typescript
|
||||||
|
import { normalizeFileInput } from '@/blocks/utils'
|
||||||
|
const file = normalizeFileInput(params.uploadFile || params.fileRef, { single: true })
|
||||||
|
if (file) params.file = file
|
||||||
|
```
|
||||||
|
|
||||||
|
For file uploads, create an internal API route (`/api/tools/{service}/upload`) that uses `downloadFileFromStorage` to get file content from `UserFile` objects.
|
||||||
|
|
||||||
### 3. Icon (`components/icons.tsx`)
|
### 3. Icon (`components/icons.tsx`)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -293,3 +310,5 @@ Register in `triggers/registry.ts`.
|
|||||||
- [ ] Create block in `blocks/blocks/{service}.ts`
|
- [ ] Create block in `blocks/blocks/{service}.ts`
|
||||||
- [ ] Register block in `blocks/registry.ts`
|
- [ ] Register block in `blocks/registry.ts`
|
||||||
- [ ] (Optional) Create and register triggers
|
- [ ] (Optional) Create and register triggers
|
||||||
|
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
|
||||||
|
- [ ] (If file uploads) Use `normalizeFileInput` in block config
|
||||||
|
|||||||
@@ -5421,3 +5421,18 @@ z'
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function EnrichSoIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 398 394' fill='none'>
|
||||||
|
<path
|
||||||
|
fill='#5A52F4'
|
||||||
|
d='M129.705566,319.705719 C127.553314,322.684906 125.651512,325.414673 123.657059,328.277466 C113.748466,318.440308 105.605003,310.395905 97.510834,302.302216 C93.625801,298.417419 89.990181,294.269318 85.949242,290.558868 C82.857994,287.720428 82.464081,285.757660 85.772888,282.551880 C104.068108,264.826202 122.146088,246.876312 140.285110,228.989670 C141.183945,228.103317 141.957443,227.089844 143.588837,225.218384 C140.691605,225.066116 138.820053,224.882874 136.948410,224.881958 C102.798264,224.865326 68.647453,224.765244 34.498699,224.983612 C29.315699,225.016739 27.990419,223.343155 28.090912,218.397430 C28.381887,204.076935 28.189890,189.746719 28.195684,175.420319 C28.198524,168.398178 28.319166,168.279541 35.590389,168.278687 C69.074188,168.274780 102.557991,168.281174 136.041794,168.266083 C137.968231,168.265213 139.894608,168.107101 141.821030,168.022171 C142.137955,167.513992 142.454895,167.005829 142.771820,166.497650 C122.842415,146.495621 102.913002,126.493591 83.261360,106.770348 C96.563828,93.471756 109.448814,80.590523 122.656265,67.386925 C123.522743,68.161835 124.785545,69.187096 125.930321,70.330513 C144.551819,88.930206 163.103683,107.600082 181.805267,126.118790 C186.713593,130.979126 189.085648,136.448059 189.055374,143.437057 C188.899490,179.418961 188.911179,215.402191 189.046661,251.384262 C189.072296,258.190796 186.742920,263.653717 181.982727,268.323273 C164.624405,285.351227 147.295807,302.409485 129.705566,319.705719z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#5A52F4'
|
||||||
|
d='M276.070923,246.906128 C288.284363,258.985870 300.156097,270.902100 312.235931,282.603485 C315.158752,285.434784 315.417542,287.246246 312.383484,290.248932 C301.143494,301.372498 290.168549,312.763733 279.075592,324.036255 C278.168030,324.958496 277.121307,325.743835 275.898315,326.801086 C274.628357,325.711792 273.460663,324.822968 272.422150,323.802673 C253.888397,305.594757 235.418701,287.321289 216.818268,269.181854 C211.508789,264.003937 208.872726,258.136688 208.914001,250.565842 C209.108337,214.917786 209.084808,179.267715 208.928864,143.619293 C208.898407,136.654907 211.130066,131.122162 216.052216,126.246094 C234.867538,107.606842 253.537521,88.820908 272.274780,70.102730 C273.313202,69.065353 274.468597,68.145027 275.264038,67.440727 C288.353516,80.579514 301.213470,93.487869 314.597534,106.922356 C295.163391,126.421753 275.214752,146.437363 255.266113,166.452972 C255.540176,166.940353 255.814240,167.427734 256.088318,167.915100 C257.983887,168.035736 259.879425,168.260345 261.775085,168.261551 C295.425201,168.282852 329.075287,168.273544 362.725403,168.279831 C369.598907,168.281113 369.776215,168.463593 369.778931,175.252213 C369.784882,189.911667 369.646088,204.573074 369.861206,219.229355 C369.925110,223.585022 368.554596,224.976288 364.148865,224.956406 C329.833130,224.801605 295.516388,224.869598 261.199951,224.868744 C259.297974,224.868698 257.396027,224.868744 254.866638,224.868744 C262.350708,232.658707 269.078217,239.661194 276.070923,246.906128z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
DynamoDBIcon,
|
DynamoDBIcon,
|
||||||
ElasticsearchIcon,
|
ElasticsearchIcon,
|
||||||
ElevenLabsIcon,
|
ElevenLabsIcon,
|
||||||
|
EnrichSoIcon,
|
||||||
ExaAIIcon,
|
ExaAIIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FirecrawlIcon,
|
FirecrawlIcon,
|
||||||
@@ -160,10 +161,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
|||||||
dynamodb: DynamoDBIcon,
|
dynamodb: DynamoDBIcon,
|
||||||
elasticsearch: ElasticsearchIcon,
|
elasticsearch: ElasticsearchIcon,
|
||||||
elevenlabs: ElevenLabsIcon,
|
elevenlabs: ElevenLabsIcon,
|
||||||
|
enrich: EnrichSoIcon,
|
||||||
exa: ExaAIIcon,
|
exa: ExaAIIcon,
|
||||||
file_v2: DocumentIcon,
|
file_v3: DocumentIcon,
|
||||||
firecrawl: FirecrawlIcon,
|
firecrawl: FirecrawlIcon,
|
||||||
fireflies: FirefliesIcon,
|
fireflies_v2: FirefliesIcon,
|
||||||
github_v2: GithubIcon,
|
github_v2: GithubIcon,
|
||||||
gitlab: GitLabIcon,
|
gitlab: GitLabIcon,
|
||||||
gmail_v2: GmailIcon,
|
gmail_v2: GmailIcon,
|
||||||
@@ -175,7 +177,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
|||||||
google_maps: GoogleMapsIcon,
|
google_maps: GoogleMapsIcon,
|
||||||
google_search: GoogleIcon,
|
google_search: GoogleIcon,
|
||||||
google_sheets_v2: GoogleSheetsIcon,
|
google_sheets_v2: GoogleSheetsIcon,
|
||||||
google_slides: GoogleSlidesIcon,
|
google_slides_v2: GoogleSlidesIcon,
|
||||||
google_vault: GoogleVaultIcon,
|
google_vault: GoogleVaultIcon,
|
||||||
grafana: GrafanaIcon,
|
grafana: GrafanaIcon,
|
||||||
grain: GrainIcon,
|
grain: GrainIcon,
|
||||||
@@ -204,7 +206,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
|||||||
microsoft_excel_v2: MicrosoftExcelIcon,
|
microsoft_excel_v2: MicrosoftExcelIcon,
|
||||||
microsoft_planner: MicrosoftPlannerIcon,
|
microsoft_planner: MicrosoftPlannerIcon,
|
||||||
microsoft_teams: MicrosoftTeamsIcon,
|
microsoft_teams: MicrosoftTeamsIcon,
|
||||||
mistral_parse_v2: MistralIcon,
|
mistral_parse_v3: MistralIcon,
|
||||||
mongodb: MongoDBIcon,
|
mongodb: MongoDBIcon,
|
||||||
mysql: MySQLIcon,
|
mysql: MySQLIcon,
|
||||||
neo4j: Neo4jIcon,
|
neo4j: Neo4jIcon,
|
||||||
@@ -219,11 +221,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
|||||||
polymarket: PolymarketIcon,
|
polymarket: PolymarketIcon,
|
||||||
postgresql: PostgresIcon,
|
postgresql: PostgresIcon,
|
||||||
posthog: PosthogIcon,
|
posthog: PosthogIcon,
|
||||||
pulse: PulseIcon,
|
pulse_v2: PulseIcon,
|
||||||
qdrant: QdrantIcon,
|
qdrant: QdrantIcon,
|
||||||
rds: RDSIcon,
|
rds: RDSIcon,
|
||||||
reddit: RedditIcon,
|
reddit: RedditIcon,
|
||||||
reducto: ReductoIcon,
|
reducto_v2: ReductoIcon,
|
||||||
resend: ResendIcon,
|
resend: ResendIcon,
|
||||||
s3: S3Icon,
|
s3: S3Icon,
|
||||||
salesforce: SalesforceIcon,
|
salesforce: SalesforceIcon,
|
||||||
@@ -242,11 +244,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
|||||||
ssh: SshIcon,
|
ssh: SshIcon,
|
||||||
stagehand: StagehandIcon,
|
stagehand: StagehandIcon,
|
||||||
stripe: StripeIcon,
|
stripe: StripeIcon,
|
||||||
stt: STTIcon,
|
stt_v2: STTIcon,
|
||||||
supabase: SupabaseIcon,
|
supabase: SupabaseIcon,
|
||||||
tavily: TavilyIcon,
|
tavily: TavilyIcon,
|
||||||
telegram: TelegramIcon,
|
telegram: TelegramIcon,
|
||||||
textract: TextractIcon,
|
textract_v2: TextractIcon,
|
||||||
tinybird: TinybirdIcon,
|
tinybird: TinybirdIcon,
|
||||||
translate: TranslateIcon,
|
translate: TranslateIcon,
|
||||||
trello: TrelloIcon,
|
trello: TrelloIcon,
|
||||||
@@ -255,7 +257,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
|||||||
twilio_voice: TwilioIcon,
|
twilio_voice: TwilioIcon,
|
||||||
typeform: TypeformIcon,
|
typeform: TypeformIcon,
|
||||||
video_generator_v2: VideoIcon,
|
video_generator_v2: VideoIcon,
|
||||||
vision: EyeIcon,
|
vision_v2: EyeIcon,
|
||||||
wealthbox: WealthboxIcon,
|
wealthbox: WealthboxIcon,
|
||||||
webflow: WebflowIcon,
|
webflow: WebflowIcon,
|
||||||
whatsapp: WhatsAppIcon,
|
whatsapp: WhatsAppIcon,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Mehrere Dateien lesen und parsen
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="file"
|
type="file_v3"
|
||||||
color="#40916C"
|
color="#40916C"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Interagieren Sie mit Fireflies.ai-Besprechungstranskripten und -auf
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="fireflies"
|
type="fireflies_v2"
|
||||||
color="#100730"
|
color="#100730"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Text aus PDF-Dokumenten extrahieren
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="mistral_parse"
|
type="mistral_parse_v3"
|
||||||
color="#000000"
|
color="#000000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -27,16 +27,16 @@ All API responses include information about your workflow execution limits and u
|
|||||||
"limits": {
|
"limits": {
|
||||||
"workflowExecutionRateLimit": {
|
"workflowExecutionRateLimit": {
|
||||||
"sync": {
|
"sync": {
|
||||||
"requestsPerMinute": 60, // Sustained rate limit per minute
|
"requestsPerMinute": 150, // Sustained rate limit per minute
|
||||||
"maxBurst": 120, // Maximum burst capacity
|
"maxBurst": 300, // Maximum burst capacity
|
||||||
"remaining": 118, // Current tokens available (up to maxBurst)
|
"remaining": 298, // Current tokens available (up to maxBurst)
|
||||||
"resetAt": "..." // When tokens next refill
|
"resetAt": "..." // When tokens next refill
|
||||||
},
|
},
|
||||||
"async": {
|
"async": {
|
||||||
"requestsPerMinute": 200, // Sustained rate limit per minute
|
"requestsPerMinute": 1000, // Sustained rate limit per minute
|
||||||
"maxBurst": 400, // Maximum burst capacity
|
"maxBurst": 2000, // Maximum burst capacity
|
||||||
"remaining": 398, // Current tokens available
|
"remaining": 1998, // Current tokens available
|
||||||
"resetAt": "..." // When tokens next refill
|
"resetAt": "..." // When tokens next refill
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"usage": {
|
"usage": {
|
||||||
@@ -107,28 +107,28 @@ Query workflow execution logs with extensive filtering options.
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nextCursor": "eyJzIjoiMjAyNS0wMS0wMVQxMjozNDo1Ni43ODlaIiwiaWQiOiJsb2dfYWJjMTIzIn0",
|
"nextCursor": "eyJzIjoiMjAyNS0wMS0wMVQxMjozNDo1Ni43ODlaIiwiaWQiOiJsb2dfYWJjMTIzIn0",
|
||||||
"limits": {
|
"limits": {
|
||||||
"workflowExecutionRateLimit": {
|
"workflowExecutionRateLimit": {
|
||||||
"sync": {
|
"sync": {
|
||||||
"requestsPerMinute": 60,
|
"requestsPerMinute": 150,
|
||||||
"maxBurst": 120,
|
"maxBurst": 300,
|
||||||
"remaining": 118,
|
"remaining": 298,
|
||||||
"resetAt": "2025-01-01T12:35:56.789Z"
|
"resetAt": "2025-01-01T12:35:56.789Z"
|
||||||
|
},
|
||||||
|
"async": {
|
||||||
|
"requestsPerMinute": 1000,
|
||||||
|
"maxBurst": 2000,
|
||||||
|
"remaining": 1998,
|
||||||
|
"resetAt": "2025-01-01T12:35:56.789Z"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"async": {
|
"usage": {
|
||||||
"requestsPerMinute": 200,
|
"currentPeriodCost": 1.234,
|
||||||
"maxBurst": 400,
|
"limit": 10,
|
||||||
"remaining": 398,
|
"plan": "pro",
|
||||||
"resetAt": "2025-01-01T12:35:56.789Z"
|
"isExceeded": false
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"usage": {
|
|
||||||
"currentPeriodCost": 1.234,
|
|
||||||
"limit": 10,
|
|
||||||
"plan": "pro",
|
|
||||||
"isExceeded": false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -188,15 +188,15 @@ Retrieve detailed information about a specific log entry.
|
|||||||
"limits": {
|
"limits": {
|
||||||
"workflowExecutionRateLimit": {
|
"workflowExecutionRateLimit": {
|
||||||
"sync": {
|
"sync": {
|
||||||
"requestsPerMinute": 60,
|
"requestsPerMinute": 150,
|
||||||
"maxBurst": 120,
|
"maxBurst": 300,
|
||||||
"remaining": 118,
|
"remaining": 298,
|
||||||
"resetAt": "2025-01-01T12:35:56.789Z"
|
"resetAt": "2025-01-01T12:35:56.789Z"
|
||||||
},
|
},
|
||||||
"async": {
|
"async": {
|
||||||
"requestsPerMinute": 200,
|
"requestsPerMinute": 1000,
|
||||||
"maxBurst": 400,
|
"maxBurst": 2000,
|
||||||
"remaining": 398,
|
"remaining": 1998,
|
||||||
"resetAt": "2025-01-01T12:35:56.789Z"
|
"resetAt": "2025-01-01T12:35:56.789Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -477,10 +477,10 @@ The API uses a **token bucket algorithm** for rate limiting, providing fair usag
|
|||||||
|
|
||||||
| Plan | Requests/Minute | Burst Capacity |
|
| Plan | Requests/Minute | Burst Capacity |
|
||||||
|------|-----------------|----------------|
|
|------|-----------------|----------------|
|
||||||
| Free | 10 | 20 |
|
| Free | 30 | 60 |
|
||||||
| Pro | 30 | 60 |
|
| Pro | 100 | 200 |
|
||||||
| Team | 60 | 120 |
|
| Team | 200 | 400 |
|
||||||
| Enterprise | 120 | 240 |
|
| Enterprise | 500 | 1000 |
|
||||||
|
|
||||||
**How it works:**
|
**How it works:**
|
||||||
- Tokens refill at `requestsPerMinute` rate
|
- Tokens refill at `requestsPerMinute` rate
|
||||||
|
|||||||
@@ -170,16 +170,16 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
|
|||||||
"rateLimit": {
|
"rateLimit": {
|
||||||
"sync": {
|
"sync": {
|
||||||
"isLimited": false,
|
"isLimited": false,
|
||||||
"requestsPerMinute": 25,
|
"requestsPerMinute": 150,
|
||||||
"maxBurst": 50,
|
"maxBurst": 300,
|
||||||
"remaining": 50,
|
"remaining": 300,
|
||||||
"resetAt": "2025-09-08T22:51:55.999Z"
|
"resetAt": "2025-09-08T22:51:55.999Z"
|
||||||
},
|
},
|
||||||
"async": {
|
"async": {
|
||||||
"isLimited": false,
|
"isLimited": false,
|
||||||
"requestsPerMinute": 200,
|
"requestsPerMinute": 1000,
|
||||||
"maxBurst": 400,
|
"maxBurst": 2000,
|
||||||
"remaining": 400,
|
"remaining": 2000,
|
||||||
"resetAt": "2025-09-08T22:51:56.155Z"
|
"resetAt": "2025-09-08T22:51:56.155Z"
|
||||||
},
|
},
|
||||||
"authType": "api"
|
"authType": "api"
|
||||||
@@ -206,13 +206,32 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
|
|||||||
|
|
||||||
Different subscription plans have different usage limits:
|
Different subscription plans have different usage limits:
|
||||||
|
|
||||||
| Plan | Monthly Usage Limit | Rate Limits (per minute) |
|
| Plan | Monthly Usage Included | Rate Limits (per minute) |
|
||||||
|------|-------------------|-------------------------|
|
|------|------------------------|-------------------------|
|
||||||
| **Free** | $20 | 5 sync, 10 async |
|
| **Free** | $20 | 50 sync, 200 async |
|
||||||
| **Pro** | $100 | 10 sync, 50 async |
|
| **Pro** | $20 (adjustable) | 150 sync, 1,000 async |
|
||||||
| **Team** | $500 (pooled) | 50 sync, 100 async |
|
| **Team** | $40/seat (pooled, adjustable) | 300 sync, 2,500 async |
|
||||||
| **Enterprise** | Custom | Custom |
|
| **Enterprise** | Custom | Custom |
|
||||||
|
|
||||||
|
## Execution Time Limits
|
||||||
|
|
||||||
|
Workflows have maximum execution time limits based on your subscription plan:
|
||||||
|
|
||||||
|
| Plan | Sync Execution | Async Execution |
|
||||||
|
|------|----------------|-----------------|
|
||||||
|
| **Free** | 5 minutes | 10 minutes |
|
||||||
|
| **Pro** | 50 minutes | 90 minutes |
|
||||||
|
| **Team** | 50 minutes | 90 minutes |
|
||||||
|
| **Enterprise** | 50 minutes | 90 minutes |
|
||||||
|
|
||||||
|
**Sync executions** run immediately and return results directly. These are triggered via the API with `async: false` (default) or through the UI.
|
||||||
|
**Async executions** (triggered via API with `async: true`, webhooks, or schedules) run in the background. Async time limits are up to 2x the sync limit, capped at 90 minutes.
|
||||||
|
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
If a workflow exceeds its time limit, it will be terminated and marked as failed with a timeout error. Design long-running workflows to use async execution or break them into smaller workflows.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
## Billing Model
|
## Billing Model
|
||||||
|
|
||||||
Sim uses a **base subscription + overage** billing model:
|
Sim uses a **base subscription + overage** billing model:
|
||||||
|
|||||||
168
apps/docs/content/docs/en/execution/files.mdx
Normal file
168
apps/docs/content/docs/en/execution/files.mdx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
title: Passing Files
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout } from 'fumadocs-ui/components/callout'
|
||||||
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||||
|
|
||||||
|
Sim makes it easy to work with files throughout your workflows. Blocks can receive files, process them, and pass them to other blocks seamlessly.
|
||||||
|
|
||||||
|
## File Objects
|
||||||
|
|
||||||
|
When blocks output files (like Gmail attachments, generated images, or parsed documents), they return a standardized file object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "report.pdf",
|
||||||
|
"url": "https://...",
|
||||||
|
"base64": "JVBERi0xLjQK...",
|
||||||
|
"type": "application/pdf",
|
||||||
|
"size": 245678
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can access any of these properties when referencing files from previous blocks.
|
||||||
|
|
||||||
|
## The File Block
|
||||||
|
|
||||||
|
The **File block** is the universal entry point for files in your workflows. It accepts files from any source and outputs standardized file objects that work with all integrations.
|
||||||
|
|
||||||
|
**Inputs:**
|
||||||
|
- **Uploaded files** - Drag and drop or select files directly
|
||||||
|
- **External URLs** - Any publicly accessible file URL
|
||||||
|
- **Files from other blocks** - Pass files from Gmail attachments, Slack downloads, etc.
|
||||||
|
|
||||||
|
**Outputs:**
|
||||||
|
- A list of `UserFile` objects with consistent structure (`name`, `url`, `base64`, `type`, `size`)
|
||||||
|
- `combinedContent` - Extracted text content from all files (for documents)
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
|
||||||
|
```
|
||||||
|
// Get all files from the File block
|
||||||
|
<file.files>
|
||||||
|
|
||||||
|
// Get the first file
|
||||||
|
<file.files[0]>
|
||||||
|
|
||||||
|
// Get combined text content from parsed documents
|
||||||
|
<file.combinedContent>
|
||||||
|
```
|
||||||
|
|
||||||
|
The File block automatically:
|
||||||
|
- Detects file types from URLs and extensions
|
||||||
|
- Extracts text from PDFs, CSVs, and documents
|
||||||
|
- Generates base64 encoding for binary files
|
||||||
|
- Creates presigned URLs for secure access
|
||||||
|
|
||||||
|
Use the File block when you need to normalize files from different sources before passing them to other blocks like Vision, STT, or email integrations.
|
||||||
|
|
||||||
|
## Passing Files Between Blocks
|
||||||
|
|
||||||
|
Reference files from previous blocks using the tag dropdown. Click in any file input field and type `<` to see available outputs.
|
||||||
|
|
||||||
|
**Common patterns:**
|
||||||
|
|
||||||
|
```
|
||||||
|
// Single file from a block
|
||||||
|
<gmail.attachments[0]>
|
||||||
|
|
||||||
|
// Pass the whole file object
|
||||||
|
<file_parser.files[0]>
|
||||||
|
|
||||||
|
// Access specific properties
|
||||||
|
<gmail.attachments[0].name>
|
||||||
|
<gmail.attachments[0].base64>
|
||||||
|
```
|
||||||
|
|
||||||
|
Most blocks accept the full file object and extract what they need automatically. You don't need to manually extract `base64` or `url` in most cases.
|
||||||
|
|
||||||
|
## Triggering Workflows with Files
|
||||||
|
|
||||||
|
When calling a workflow via API that expects file input, include files in your request:
|
||||||
|
|
||||||
|
<Tabs items={['Base64', 'URL']}>
|
||||||
|
<Tab value="Base64">
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: YOUR_API_KEY" \
|
||||||
|
-d '{
|
||||||
|
"document": {
|
||||||
|
"name": "report.pdf",
|
||||||
|
"base64": "JVBERi0xLjQK...",
|
||||||
|
"type": "application/pdf"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab value="URL">
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: YOUR_API_KEY" \
|
||||||
|
-d '{
|
||||||
|
"document": {
|
||||||
|
"name": "report.pdf",
|
||||||
|
"url": "https://example.com/report.pdf",
|
||||||
|
"type": "application/pdf"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
The workflow's Start block should have an input field configured to receive the file parameter.
|
||||||
|
|
||||||
|
## Receiving Files in API Responses
|
||||||
|
|
||||||
|
When a workflow outputs files, they're included in the response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"output": {
|
||||||
|
"generatedFile": {
|
||||||
|
"name": "output.png",
|
||||||
|
"url": "https://...",
|
||||||
|
"base64": "iVBORw0KGgo...",
|
||||||
|
"type": "image/png",
|
||||||
|
"size": 34567
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `url` for direct downloads or `base64` for inline processing.
|
||||||
|
|
||||||
|
## Blocks That Work with Files
|
||||||
|
|
||||||
|
**File inputs:**
|
||||||
|
- **File** - Parse documents, images, and text files
|
||||||
|
- **Vision** - Analyze images with AI models
|
||||||
|
- **Mistral Parser** - Extract text from PDFs
|
||||||
|
|
||||||
|
**File outputs:**
|
||||||
|
- **Gmail** - Email attachments
|
||||||
|
- **Slack** - Downloaded files
|
||||||
|
- **TTS** - Generated audio files
|
||||||
|
- **Video Generator** - Generated videos
|
||||||
|
- **Image Generator** - Generated images
|
||||||
|
|
||||||
|
**File storage:**
|
||||||
|
- **Supabase** - Upload/download from storage
|
||||||
|
- **S3** - AWS S3 operations
|
||||||
|
- **Google Drive** - Drive file operations
|
||||||
|
- **Dropbox** - Dropbox file operations
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
Files are automatically available to downstream blocks. The execution engine handles all file transfer and format conversion.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use file objects directly** - Pass the full file object rather than extracting individual properties. Blocks handle the conversion automatically.
|
||||||
|
|
||||||
|
2. **Check file types** - Ensure the file type matches what the receiving block expects. The Vision block needs images, the File block handles documents.
|
||||||
|
|
||||||
|
3. **Consider file size** - Large files increase execution time. For very large files, consider using storage blocks (S3, Supabase) for intermediate storage.
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"pages": ["index", "basics", "api", "logging", "costs"]
|
"pages": ["index", "basics", "files", "api", "logging", "costs"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ A quick lookup for everyday actions in the Sim workflow editor. For keyboard sho
|
|||||||
<td>Right-click → **Enable/Disable**</td>
|
<td>Right-click → **Enable/Disable**</td>
|
||||||
<td><ActionImage src="/static/quick-reference/disable-block.png" alt="Disable block" /></td>
|
<td><ActionImage src="/static/quick-reference/disable-block.png" alt="Disable block" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Lock/Unlock a block</td>
|
||||||
|
<td>Hover block → Click lock icon (Admin only)</td>
|
||||||
|
<td><ActionImage src="/static/quick-reference/lock-block.png" alt="Lock block" /></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Toggle handle orientation</td>
|
<td>Toggle handle orientation</td>
|
||||||
<td>Right-click → **Toggle Handles**</td>
|
<td>Right-click → **Toggle Handles**</td>
|
||||||
|
|||||||
@@ -49,10 +49,25 @@ Retrieve content from Confluence pages using the Confluence API.
|
|||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `ts` | string | Timestamp of retrieval |
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
| `pageId` | string | Confluence page ID |
|
| `pageId` | string | Confluence page ID |
|
||||||
| `content` | string | Page content with HTML tags stripped |
|
|
||||||
| `title` | string | Page title |
|
| `title` | string | Page title |
|
||||||
|
| `content` | string | Page content with HTML tags stripped |
|
||||||
|
| `status` | string | Page status \(current, archived, trashed, draft\) |
|
||||||
|
| `spaceId` | string | ID of the space containing the page |
|
||||||
|
| `parentId` | string | ID of the parent page |
|
||||||
|
| `authorId` | string | Account ID of the page author |
|
||||||
|
| `createdAt` | string | ISO 8601 timestamp when the page was created |
|
||||||
|
| `url` | string | URL to view the page in Confluence |
|
||||||
|
| `body` | object | Raw page body content in storage format |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| `version` | object | Page version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
|
||||||
### `confluence_update`
|
### `confluence_update`
|
||||||
|
|
||||||
@@ -76,6 +91,25 @@ Update a Confluence page using the Confluence API.
|
|||||||
| `ts` | string | Timestamp of update |
|
| `ts` | string | Timestamp of update |
|
||||||
| `pageId` | string | Confluence page ID |
|
| `pageId` | string | Confluence page ID |
|
||||||
| `title` | string | Updated page title |
|
| `title` | string | Updated page title |
|
||||||
|
| `status` | string | Page status |
|
||||||
|
| `spaceId` | string | Space ID |
|
||||||
|
| `body` | object | Page body content in storage format |
|
||||||
|
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `view` | object | Body in view format \(rendered HTML\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| `version` | object | Page version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| `url` | string | URL to view the page in Confluence |
|
||||||
| `success` | boolean | Update operation success status |
|
| `success` | boolean | Update operation success status |
|
||||||
|
|
||||||
### `confluence_create_page`
|
### `confluence_create_page`
|
||||||
@@ -100,11 +134,30 @@ Create a new page in a Confluence space.
|
|||||||
| `ts` | string | Timestamp of creation |
|
| `ts` | string | Timestamp of creation |
|
||||||
| `pageId` | string | Created page ID |
|
| `pageId` | string | Created page ID |
|
||||||
| `title` | string | Page title |
|
| `title` | string | Page title |
|
||||||
|
| `status` | string | Page status |
|
||||||
|
| `spaceId` | string | Space ID |
|
||||||
|
| `parentId` | string | Parent page ID |
|
||||||
|
| `body` | object | Page body content |
|
||||||
|
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `view` | object | Body in view format \(rendered HTML\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| `version` | object | Page version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
| `url` | string | Page URL |
|
| `url` | string | Page URL |
|
||||||
|
|
||||||
### `confluence_delete_page`
|
### `confluence_delete_page`
|
||||||
|
|
||||||
Delete a Confluence page (moves it to trash where it can be restored).
|
Delete a Confluence page. By default moves to trash; use purge=true to permanently delete.
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
@@ -112,6 +165,7 @@ Delete a Confluence page (moves it to trash where it can be restored).
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
| `pageId` | string | Yes | Confluence page ID to delete |
|
| `pageId` | string | Yes | Confluence page ID to delete |
|
||||||
|
| `purge` | boolean | No | If true, permanently deletes the page instead of moving to trash \(default: false\) |
|
||||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
@@ -122,6 +176,229 @@ Delete a Confluence page (moves it to trash where it can be restored).
|
|||||||
| `pageId` | string | Deleted page ID |
|
| `pageId` | string | Deleted page ID |
|
||||||
| `deleted` | boolean | Deletion status |
|
| `deleted` | boolean | Deletion status |
|
||||||
|
|
||||||
|
### `confluence_list_pages_in_space`
|
||||||
|
|
||||||
|
List all pages within a specific Confluence space. Supports pagination and filtering by status.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `spaceId` | string | Yes | The ID of the Confluence space to list pages from |
|
||||||
|
| `limit` | number | No | Maximum number of pages to return \(default: 50, max: 250\) |
|
||||||
|
| `status` | string | No | Filter pages by status: current, archived, trashed, or draft |
|
||||||
|
| `bodyFormat` | string | No | Format for page body content: storage, atlas_doc_format, or view. If not specified, body is not included. |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response to get the next page of results |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `pages` | array | Array of pages in the space |
|
||||||
|
| ↳ `id` | string | Unique page identifier |
|
||||||
|
| ↳ `title` | string | Page title |
|
||||||
|
| ↳ `status` | string | Page status \(e.g., current, archived, trashed, draft\) |
|
||||||
|
| ↳ `spaceId` | string | ID of the space containing the page |
|
||||||
|
| ↳ `parentId` | string | ID of the parent page \(null if top-level\) |
|
||||||
|
| ↳ `authorId` | string | Account ID of the page author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp when the page was created |
|
||||||
|
| ↳ `version` | object | Page version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| ↳ `body` | object | Page body content \(if bodyFormat was specified\) |
|
||||||
|
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `view` | object | Body in view format \(rendered HTML\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `webUrl` | string | URL to view the page in Confluence |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
|
### `confluence_get_page_children`
|
||||||
|
|
||||||
|
Get all child pages of a specific Confluence page. Useful for navigating page hierarchies.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `pageId` | string | Yes | The ID of the parent page to get children from |
|
||||||
|
| `limit` | number | No | Maximum number of child pages to return \(default: 50, max: 250\) |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response to get the next page of results |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `parentId` | string | ID of the parent page |
|
||||||
|
| `children` | array | Array of child pages |
|
||||||
|
| ↳ `id` | string | Child page ID |
|
||||||
|
| ↳ `title` | string | Child page title |
|
||||||
|
| ↳ `status` | string | Page status |
|
||||||
|
| ↳ `spaceId` | string | Space ID |
|
||||||
|
| ↳ `childPosition` | number | Position among siblings |
|
||||||
|
| ↳ `webUrl` | string | URL to view the page |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
|
### `confluence_get_page_ancestors`
|
||||||
|
|
||||||
|
Get the ancestor (parent) pages of a specific Confluence page. Returns the full hierarchy from the page up to the root.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `pageId` | string | Yes | The ID of the page to get ancestors for |
|
||||||
|
| `limit` | number | No | Maximum number of ancestors to return \(default: 25, max: 250\) |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `pageId` | string | ID of the page whose ancestors were retrieved |
|
||||||
|
| `ancestors` | array | Array of ancestor pages, ordered from direct parent to root |
|
||||||
|
| ↳ `id` | string | Ancestor page ID |
|
||||||
|
| ↳ `title` | string | Ancestor page title |
|
||||||
|
| ↳ `status` | string | Page status |
|
||||||
|
| ↳ `spaceId` | string | Space ID |
|
||||||
|
| ↳ `webUrl` | string | URL to view the page |
|
||||||
|
|
||||||
|
### `confluence_list_page_versions`
|
||||||
|
|
||||||
|
List all versions (revision history) of a Confluence page.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `pageId` | string | Yes | The ID of the page to get versions for |
|
||||||
|
| `limit` | number | No | Maximum number of versions to return \(default: 50, max: 250\) |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `pageId` | string | ID of the page |
|
||||||
|
| `versions` | array | Array of page versions |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
|
### `confluence_get_page_version`
|
||||||
|
|
||||||
|
Get details about a specific version of a Confluence page.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `pageId` | string | Yes | The ID of the page |
|
||||||
|
| `versionNumber` | number | Yes | The version number to retrieve \(e.g., 1, 2, 3\) |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `pageId` | string | ID of the page |
|
||||||
|
| `version` | object | Detailed version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| ↳ `contentTypeModified` | boolean | Whether the content type was modified in this version |
|
||||||
|
| ↳ `collaborators` | array | List of collaborator account IDs for this version |
|
||||||
|
| ↳ `prevVersion` | number | Previous version number |
|
||||||
|
| ↳ `nextVersion` | number | Next version number |
|
||||||
|
|
||||||
|
### `confluence_list_page_properties`
|
||||||
|
|
||||||
|
List all custom properties (metadata) attached to a Confluence page.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `pageId` | string | Yes | The ID of the page to list properties from |
|
||||||
|
| `limit` | number | No | Maximum number of properties to return \(default: 50, max: 250\) |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `pageId` | string | ID of the page |
|
||||||
|
| `properties` | array | Array of content properties |
|
||||||
|
| ↳ `id` | string | Property ID |
|
||||||
|
| ↳ `key` | string | Property key |
|
||||||
|
| ↳ `value` | json | Property value \(can be any JSON\) |
|
||||||
|
| ↳ `version` | object | Version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
|
### `confluence_create_page_property`
|
||||||
|
|
||||||
|
Create a new custom property (metadata) on a Confluence page.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `pageId` | string | Yes | The ID of the page to add the property to |
|
||||||
|
| `key` | string | Yes | The key/name for the property |
|
||||||
|
| `value` | json | Yes | The value for the property \(can be any JSON value\) |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `pageId` | string | ID of the page |
|
||||||
|
| `propertyId` | string | ID of the created property |
|
||||||
|
| `key` | string | Property key |
|
||||||
|
| `value` | json | Property value |
|
||||||
|
| `version` | object | Version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
|
||||||
### `confluence_search`
|
### `confluence_search`
|
||||||
|
|
||||||
Search for content across Confluence pages, blog posts, and other content.
|
Search for content across Confluence pages, blog posts, and other content.
|
||||||
@@ -155,6 +432,211 @@ Search for content across Confluence pages, blog posts, and other content.
|
|||||||
| ↳ `lastModified` | string | ISO 8601 timestamp of last modification |
|
| ↳ `lastModified` | string | ISO 8601 timestamp of last modification |
|
||||||
| ↳ `entityType` | string | Entity type identifier \(e.g., content, space\) |
|
| ↳ `entityType` | string | Entity type identifier \(e.g., content, space\) |
|
||||||
|
|
||||||
|
### `confluence_search_in_space`
|
||||||
|
|
||||||
|
Search for content within a specific Confluence space. Optionally filter by text query and content type.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `spaceKey` | string | Yes | The key of the Confluence space to search in \(e.g., "ENG", "HR"\) |
|
||||||
|
| `query` | string | No | Text search query. If not provided, returns all content in the space. |
|
||||||
|
| `contentType` | string | No | Filter by content type: page, blogpost, attachment, or comment |
|
||||||
|
| `limit` | number | No | Maximum number of results to return \(default: 25, max: 250\) |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `spaceKey` | string | The space key that was searched |
|
||||||
|
| `totalSize` | number | Total number of matching results |
|
||||||
|
| `results` | array | Array of search results |
|
||||||
|
| ↳ `id` | string | Unique content identifier |
|
||||||
|
| ↳ `title` | string | Content title |
|
||||||
|
| ↳ `type` | string | Content type \(e.g., page, blogpost, attachment, comment\) |
|
||||||
|
| ↳ `status` | string | Content status \(e.g., current\) |
|
||||||
|
| ↳ `url` | string | URL to view the content in Confluence |
|
||||||
|
| ↳ `excerpt` | string | Text excerpt matching the search query |
|
||||||
|
| ↳ `spaceKey` | string | Key of the space containing the content |
|
||||||
|
| ↳ `space` | object | Space information for the content |
|
||||||
|
| ↳ `id` | string | Space identifier |
|
||||||
|
| ↳ `key` | string | Space key |
|
||||||
|
| ↳ `name` | string | Space name |
|
||||||
|
| ↳ `lastModified` | string | ISO 8601 timestamp of last modification |
|
||||||
|
| ↳ `entityType` | string | Entity type identifier \(e.g., content, space\) |
|
||||||
|
|
||||||
|
### `confluence_list_blogposts`
|
||||||
|
|
||||||
|
List all blog posts across all accessible Confluence spaces.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `limit` | number | No | Maximum number of blog posts to return \(default: 25, max: 250\) |
|
||||||
|
| `status` | string | No | Filter by status: current, archived, trashed, or draft |
|
||||||
|
| `sort` | string | No | Sort order: created-date, -created-date, modified-date, -modified-date, title, -title |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `blogPosts` | array | Array of blog posts |
|
||||||
|
| ↳ `id` | string | Blog post ID |
|
||||||
|
| ↳ `title` | string | Blog post title |
|
||||||
|
| ↳ `status` | string | Blog post status |
|
||||||
|
| ↳ `spaceId` | string | Space ID |
|
||||||
|
| ↳ `authorId` | string | Author account ID |
|
||||||
|
| ↳ `createdAt` | string | Creation timestamp |
|
||||||
|
| ↳ `version` | object | Version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| ↳ `webUrl` | string | URL to view the blog post |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
|
### `confluence_get_blogpost`
|
||||||
|
|
||||||
|
Get a specific Confluence blog post by ID, including its content.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `blogPostId` | string | Yes | The ID of the blog post to retrieve |
|
||||||
|
| `bodyFormat` | string | No | Format for blog post body: storage, atlas_doc_format, or view |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `id` | string | Blog post ID |
|
||||||
|
| `title` | string | Blog post title |
|
||||||
|
| `status` | string | Blog post status |
|
||||||
|
| `spaceId` | string | Space ID |
|
||||||
|
| `authorId` | string | Author account ID |
|
||||||
|
| `createdAt` | string | Creation timestamp |
|
||||||
|
| `version` | object | Version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| `body` | object | Blog post body content in requested format\(s\) |
|
||||||
|
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `view` | object | Body in view format \(rendered HTML\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| `webUrl` | string | URL to view the blog post |
|
||||||
|
|
||||||
|
### `confluence_create_blogpost`
|
||||||
|
|
||||||
|
Create a new blog post in a Confluence space.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `spaceId` | string | Yes | The ID of the space to create the blog post in |
|
||||||
|
| `title` | string | Yes | Title of the blog post |
|
||||||
|
| `content` | string | Yes | Blog post content in Confluence storage format \(HTML\) |
|
||||||
|
| `status` | string | No | Blog post status: current \(default\) or draft |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `id` | string | Created blog post ID |
|
||||||
|
| `title` | string | Blog post title |
|
||||||
|
| `status` | string | Blog post status |
|
||||||
|
| `spaceId` | string | Space ID |
|
||||||
|
| `authorId` | string | Author account ID |
|
||||||
|
| `body` | object | Blog post body content |
|
||||||
|
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `view` | object | Body in view format \(rendered HTML\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| `version` | object | Blog post version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| `webUrl` | string | URL to view the blog post |
|
||||||
|
|
||||||
|
### `confluence_list_blogposts_in_space`
|
||||||
|
|
||||||
|
List all blog posts within a specific Confluence space.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `spaceId` | string | Yes | The ID of the Confluence space to list blog posts from |
|
||||||
|
| `limit` | number | No | Maximum number of blog posts to return \(default: 25, max: 250\) |
|
||||||
|
| `status` | string | No | Filter by status: current, archived, trashed, or draft |
|
||||||
|
| `bodyFormat` | string | No | Format for blog post body: storage, atlas_doc_format, or view |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `blogPosts` | array | Array of blog posts in the space |
|
||||||
|
| ↳ `id` | string | Blog post ID |
|
||||||
|
| ↳ `title` | string | Blog post title |
|
||||||
|
| ↳ `status` | string | Blog post status |
|
||||||
|
| ↳ `spaceId` | string | Space ID |
|
||||||
|
| ↳ `authorId` | string | Author account ID |
|
||||||
|
| ↳ `createdAt` | string | Creation timestamp |
|
||||||
|
| ↳ `version` | object | Version information |
|
||||||
|
| ↳ `number` | number | Version number |
|
||||||
|
| ↳ `message` | string | Version message |
|
||||||
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| ↳ `body` | object | Blog post body content |
|
||||||
|
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `view` | object | Body in view format \(rendered HTML\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
|
||||||
|
| ↳ `value` | string | The content value in the specified format |
|
||||||
|
| ↳ `representation` | string | Content representation type |
|
||||||
|
| ↳ `webUrl` | string | URL to view the blog post |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
### `confluence_create_comment`
|
### `confluence_create_comment`
|
||||||
|
|
||||||
Add a comment to a Confluence page.
|
Add a comment to a Confluence page.
|
||||||
@@ -187,6 +669,8 @@ List all comments on a Confluence page.
|
|||||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
| `pageId` | string | Yes | Confluence page ID to list comments from |
|
| `pageId` | string | Yes | Confluence page ID to list comments from |
|
||||||
| `limit` | number | No | Maximum number of comments to return \(default: 25\) |
|
| `limit` | number | No | Maximum number of comments to return \(default: 25\) |
|
||||||
|
| `bodyFormat` | string | No | Format for the comment body: storage, atlas_doc_format, view, or export_view \(default: storage\) |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response |
|
||||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
@@ -212,6 +696,7 @@ List all comments on a Confluence page.
|
|||||||
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
| ↳ `authorId` | string | Account ID of the version author |
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
### `confluence_update_comment`
|
### `confluence_update_comment`
|
||||||
|
|
||||||
@@ -291,7 +776,8 @@ List all attachments on a Confluence page.
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
| `pageId` | string | Yes | Confluence page ID to list attachments from |
|
| `pageId` | string | Yes | Confluence page ID to list attachments from |
|
||||||
| `limit` | number | No | Maximum number of attachments to return \(default: 25\) |
|
| `limit` | number | No | Maximum number of attachments to return \(default: 50, max: 250\) |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response |
|
||||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
@@ -316,6 +802,7 @@ List all attachments on a Confluence page.
|
|||||||
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
|
||||||
| ↳ `authorId` | string | Account ID of the version author |
|
| ↳ `authorId` | string | Account ID of the version author |
|
||||||
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
### `confluence_delete_attachment`
|
### `confluence_delete_attachment`
|
||||||
|
|
||||||
@@ -347,6 +834,8 @@ List all labels on a Confluence page.
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
| `pageId` | string | Yes | Confluence page ID to list labels from |
|
| `pageId` | string | Yes | Confluence page ID to list labels from |
|
||||||
|
| `limit` | number | No | Maximum number of labels to return \(default: 25, max: 250\) |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response |
|
||||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
@@ -358,6 +847,30 @@ List all labels on a Confluence page.
|
|||||||
| ↳ `id` | string | Unique label identifier |
|
| ↳ `id` | string | Unique label identifier |
|
||||||
| ↳ `name` | string | Label name |
|
| ↳ `name` | string | Label name |
|
||||||
| ↳ `prefix` | string | Label prefix/type \(e.g., global, my, team\) |
|
| ↳ `prefix` | string | Label prefix/type \(e.g., global, my, team\) |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
|
### `confluence_add_label`
|
||||||
|
|
||||||
|
Add a label to a Confluence page for organization and categorization.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `pageId` | string | Yes | Confluence page ID to add the label to |
|
||||||
|
| `labelName` | string | Yes | Name of the label to add |
|
||||||
|
| `prefix` | string | No | Label prefix: global \(default\), my, team, or system |
|
||||||
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
|
| `pageId` | string | Page ID that the label was added to |
|
||||||
|
| `labelName` | string | Name of the added label |
|
||||||
|
| `labelId` | string | ID of the added label |
|
||||||
|
|
||||||
### `confluence_get_space`
|
### `confluence_get_space`
|
||||||
|
|
||||||
@@ -375,13 +888,19 @@ Get details about a specific Confluence space.
|
|||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `ts` | string | Timestamp of retrieval |
|
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||||
| `spaceId` | string | Space ID |
|
| `spaceId` | string | Space ID |
|
||||||
| `name` | string | Space name |
|
| `name` | string | Space name |
|
||||||
| `key` | string | Space key |
|
| `key` | string | Space key |
|
||||||
| `type` | string | Space type |
|
| `type` | string | Space type \(global, personal\) |
|
||||||
| `status` | string | Space status |
|
| `status` | string | Space status \(current, archived\) |
|
||||||
| `url` | string | Space URL |
|
| `url` | string | URL to view the space in Confluence |
|
||||||
|
| `authorId` | string | Account ID of the space creator |
|
||||||
|
| `createdAt` | string | ISO 8601 timestamp when the space was created |
|
||||||
|
| `homepageId` | string | ID of the space homepage |
|
||||||
|
| `description` | object | Space description content |
|
||||||
|
| ↳ `value` | string | Description text content |
|
||||||
|
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||||
|
|
||||||
### `confluence_list_spaces`
|
### `confluence_list_spaces`
|
||||||
|
|
||||||
@@ -392,7 +911,8 @@ List all Confluence spaces accessible to the user.
|
|||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
| `limit` | number | No | Maximum number of spaces to return \(default: 25\) |
|
| `limit` | number | No | Maximum number of spaces to return \(default: 25, max: 250\) |
|
||||||
|
| `cursor` | string | No | Pagination cursor from previous response |
|
||||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
@@ -412,5 +932,6 @@ List all Confluence spaces accessible to the user.
|
|||||||
| ↳ `description` | object | Space description |
|
| ↳ `description` | object | Space description |
|
||||||
| ↳ `value` | string | Description text content |
|
| ↳ `value` | string | Description text content |
|
||||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||||
|
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ Send a message to a Discord channel
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `message` | string | Success or error message |
|
| `message` | string | Success or error message |
|
||||||
|
| `files` | file[] | Files attached to the message |
|
||||||
| `data` | object | Discord message data |
|
| `data` | object | Discord message data |
|
||||||
| ↳ `id` | string | Message ID |
|
| ↳ `id` | string | Message ID |
|
||||||
| ↳ `content` | string | Message content |
|
| ↳ `content` | string | Message content |
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ Upload a file to Dropbox
|
|||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `path` | string | Yes | The path in Dropbox where the file should be saved \(e.g., /folder/document.pdf\) |
|
| `path` | string | Yes | The path in Dropbox where the file should be saved \(e.g., /folder/document.pdf\) |
|
||||||
| `fileContent` | string | Yes | The base64 encoded content of the file to upload |
|
| `file` | file | No | The file to upload \(UserFile object\) |
|
||||||
|
| `fileContent` | string | No | Legacy: base64 encoded file content |
|
||||||
| `fileName` | string | No | Optional filename \(used if path is a folder\) |
|
| `fileName` | string | No | Optional filename \(used if path is a folder\) |
|
||||||
| `mode` | string | No | Write mode: add \(default\) or overwrite |
|
| `mode` | string | No | Write mode: add \(default\) or overwrite |
|
||||||
| `autorename` | boolean | No | If true, rename the file if there is a conflict |
|
| `autorename` | boolean | No | If true, rename the file if there is a conflict |
|
||||||
@@ -66,7 +67,7 @@ Upload a file to Dropbox
|
|||||||
|
|
||||||
### `dropbox_download`
|
### `dropbox_download`
|
||||||
|
|
||||||
Download a file from Dropbox and get a temporary link
|
Download a file from Dropbox with metadata and content
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
@@ -78,11 +79,8 @@ Download a file from Dropbox and get a temporary link
|
|||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `file` | object | The file metadata |
|
| `file` | file | Downloaded file stored in execution files |
|
||||||
| ↳ `id` | string | Unique identifier for the file |
|
| `metadata` | json | The file metadata |
|
||||||
| ↳ `name` | string | Name of the file |
|
|
||||||
| ↳ `path_display` | string | Display path of the file |
|
|
||||||
| ↳ `size` | number | Size of the file in bytes |
|
|
||||||
| `temporaryLink` | string | Temporary link to download the file \(valid for ~4 hours\) |
|
| `temporaryLink` | string | Temporary link to download the file \(valid for ~4 hours\) |
|
||||||
| `content` | string | Base64 encoded file content \(if fetched\) |
|
| `content` | string | Base64 encoded file content \(if fetched\) |
|
||||||
|
|
||||||
|
|||||||
930
apps/docs/content/docs/en/tools/enrich.mdx
Normal file
930
apps/docs/content/docs/en/tools/enrich.mdx
Normal file
@@ -0,0 +1,930 @@
|
|||||||
|
---
|
||||||
|
title: Enrich
|
||||||
|
description: B2B data enrichment and LinkedIn intelligence with Enrich.so
|
||||||
|
---
|
||||||
|
|
||||||
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
|
<BlockInfoCard
|
||||||
|
type="enrich"
|
||||||
|
color="#E5E5E6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
|
[Enrich.so](https://enrich.so/) delivers real-time, precision B2B data enrichment and LinkedIn intelligence. Its platform provides dynamic access to public and structured company, contact, and professional information, enabling teams to build richer profiles, improve lead quality, and drive more effective outreach.
|
||||||
|
|
||||||
|
With Enrich.so, you can:
|
||||||
|
|
||||||
|
- **Enrich contact and company profiles**: Instantly discover key data points for leads, prospects, and businesses using just an email or LinkedIn profile.
|
||||||
|
- **Verify email deliverability**: Check if emails are valid, deliverable, and safe to contact before sending.
|
||||||
|
- **Find work & personal emails**: Identify missing business emails from a LinkedIn profile or personal emails to expand your reach.
|
||||||
|
- **Reveal phone numbers and social profiles**: Surface additional communication channels for contacts through enrichment tools.
|
||||||
|
- **Analyze LinkedIn posts and engagement**: Extract insights on post reach, reactions, and audience from public LinkedIn content.
|
||||||
|
- **Conduct advanced people and company search**: Enable your agents to locate companies and professionals based on deep filters and real-time intelligence.
|
||||||
|
|
||||||
|
The Sim integration with Enrich.so empowers your agents and automations to instantly query, enrich, and validate B2B data, boosting productivity in workflows like sales prospecting, recruiting, marketing operations, and more. Combining Sim's orchestration capabilities with Enrich.so unlocks smarter, data-driven automation strategies powered by best-in-class B2B intelligence.
|
||||||
|
{/* MANUAL-CONTENT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Instructions
|
||||||
|
|
||||||
|
Access real-time B2B data intelligence with Enrich.so. Enrich profiles from email addresses, find work emails from LinkedIn, verify email deliverability, search for people and companies, and analyze LinkedIn post engagement.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
### `enrich_check_credits`
|
||||||
|
|
||||||
|
Check your Enrich API credit usage and remaining balance.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `totalCredits` | number | Total credits allocated to the account |
|
||||||
|
| `creditsUsed` | number | Credits consumed so far |
|
||||||
|
| `creditsRemaining` | number | Available credits remaining |
|
||||||
|
|
||||||
|
### `enrich_email_to_profile`
|
||||||
|
|
||||||
|
Retrieve detailed LinkedIn profile information using an email address including work history, education, and skills.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `email` | string | Yes | Email address to look up \(e.g., john.doe@company.com\) |
|
||||||
|
| `inRealtime` | boolean | No | Set to true to retrieve fresh data, bypassing cached information |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `displayName` | string | Full display name |
|
||||||
|
| `firstName` | string | First name |
|
||||||
|
| `lastName` | string | Last name |
|
||||||
|
| `headline` | string | Professional headline |
|
||||||
|
| `occupation` | string | Current occupation |
|
||||||
|
| `summary` | string | Profile summary |
|
||||||
|
| `location` | string | Location |
|
||||||
|
| `country` | string | Country |
|
||||||
|
| `linkedInUrl` | string | LinkedIn profile URL |
|
||||||
|
| `photoUrl` | string | Profile photo URL |
|
||||||
|
| `connectionCount` | number | Number of connections |
|
||||||
|
| `isConnectionCountObfuscated` | boolean | Whether connection count is obfuscated \(500+\) |
|
||||||
|
| `positionHistory` | array | Work experience history |
|
||||||
|
| ↳ `title` | string | Job title |
|
||||||
|
| ↳ `company` | string | Company name |
|
||||||
|
| ↳ `startDate` | string | Start date |
|
||||||
|
| ↳ `endDate` | string | End date |
|
||||||
|
| ↳ `location` | string | Location |
|
||||||
|
| `education` | array | Education history |
|
||||||
|
| ↳ `school` | string | School name |
|
||||||
|
| ↳ `degree` | string | Degree |
|
||||||
|
| ↳ `fieldOfStudy` | string | Field of study |
|
||||||
|
| ↳ `startDate` | string | Start date |
|
||||||
|
| ↳ `endDate` | string | End date |
|
||||||
|
| `certifications` | array | Professional certifications |
|
||||||
|
| ↳ `name` | string | Certification name |
|
||||||
|
| ↳ `authority` | string | Issuing authority |
|
||||||
|
| ↳ `url` | string | Certification URL |
|
||||||
|
| `skills` | array | List of skills |
|
||||||
|
| `languages` | array | List of languages |
|
||||||
|
| `locale` | string | Profile locale \(e.g., en_US\) |
|
||||||
|
| `version` | number | Profile version number |
|
||||||
|
|
||||||
|
### `enrich_email_to_person_lite`
|
||||||
|
|
||||||
|
Retrieve basic LinkedIn profile information from an email address. A lighter version with essential data only.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `email` | string | Yes | Email address to look up \(e.g., john.doe@company.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `name` | string | Full name |
|
||||||
|
| `firstName` | string | First name |
|
||||||
|
| `lastName` | string | Last name |
|
||||||
|
| `email` | string | Email address |
|
||||||
|
| `title` | string | Job title |
|
||||||
|
| `location` | string | Location |
|
||||||
|
| `company` | string | Current company |
|
||||||
|
| `companyLocation` | string | Company location |
|
||||||
|
| `companyLinkedIn` | string | Company LinkedIn URL |
|
||||||
|
| `profileId` | string | LinkedIn profile ID |
|
||||||
|
| `schoolName` | string | School name |
|
||||||
|
| `schoolUrl` | string | School URL |
|
||||||
|
| `linkedInUrl` | string | LinkedIn profile URL |
|
||||||
|
| `photoUrl` | string | Profile photo URL |
|
||||||
|
| `followerCount` | number | Number of followers |
|
||||||
|
| `connectionCount` | number | Number of connections |
|
||||||
|
| `languages` | array | Languages spoken |
|
||||||
|
| `projects` | array | Projects |
|
||||||
|
| `certifications` | array | Certifications |
|
||||||
|
| `volunteerExperience` | array | Volunteer experience |
|
||||||
|
|
||||||
|
### `enrich_linkedin_profile`
|
||||||
|
|
||||||
|
Enrich a LinkedIn profile URL with detailed information including positions, education, and social metrics.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `url` | string | Yes | LinkedIn profile URL \(e.g., linkedin.com/in/williamhgates\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `profileId` | string | LinkedIn profile ID |
|
||||||
|
| `firstName` | string | First name |
|
||||||
|
| `lastName` | string | Last name |
|
||||||
|
| `subTitle` | string | Profile subtitle/headline |
|
||||||
|
| `profilePicture` | string | Profile picture URL |
|
||||||
|
| `backgroundImage` | string | Background image URL |
|
||||||
|
| `industry` | string | Industry |
|
||||||
|
| `location` | string | Location |
|
||||||
|
| `followersCount` | number | Number of followers |
|
||||||
|
| `connectionsCount` | number | Number of connections |
|
||||||
|
| `premium` | boolean | Whether the account is premium |
|
||||||
|
| `influencer` | boolean | Whether the account is an influencer |
|
||||||
|
| `positions` | array | Work positions |
|
||||||
|
| ↳ `title` | string | Job title |
|
||||||
|
| ↳ `company` | string | Company name |
|
||||||
|
| ↳ `companyLogo` | string | Company logo URL |
|
||||||
|
| ↳ `startDate` | string | Start date |
|
||||||
|
| ↳ `endDate` | string | End date |
|
||||||
|
| ↳ `location` | string | Location |
|
||||||
|
| `education` | array | Education history |
|
||||||
|
| ↳ `school` | string | School name |
|
||||||
|
| ↳ `degree` | string | Degree |
|
||||||
|
| ↳ `fieldOfStudy` | string | Field of study |
|
||||||
|
| ↳ `startDate` | string | Start date |
|
||||||
|
| ↳ `endDate` | string | End date |
|
||||||
|
| `websites` | array | Personal websites |
|
||||||
|
|
||||||
|
### `enrich_find_email`
|
||||||
|
|
||||||
|
Find a person
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `fullName` | string | Yes | Person's full name \(e.g., John Doe\) |
|
||||||
|
| `companyDomain` | string | Yes | Company domain \(e.g., example.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `email` | string | Found email address |
|
||||||
|
| `firstName` | string | First name |
|
||||||
|
| `lastName` | string | Last name |
|
||||||
|
| `domain` | string | Company domain |
|
||||||
|
| `found` | boolean | Whether an email was found |
|
||||||
|
| `acceptAll` | boolean | Whether the domain accepts all emails |
|
||||||
|
|
||||||
|
### `enrich_linkedin_to_work_email`
|
||||||
|
|
||||||
|
Find a work email address from a LinkedIn profile URL.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `linkedinProfile` | string | Yes | LinkedIn profile URL \(e.g., https://www.linkedin.com/in/williamhgates\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `email` | string | Found work email address |
|
||||||
|
| `found` | boolean | Whether an email was found |
|
||||||
|
| `status` | string | Request status \(in_progress or completed\) |
|
||||||
|
|
||||||
|
### `enrich_linkedin_to_personal_email`
|
||||||
|
|
||||||
|
Find personal email address from a LinkedIn profile URL.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `linkedinProfile` | string | Yes | LinkedIn profile URL \(e.g., linkedin.com/in/username\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `email` | string | Personal email address |
|
||||||
|
| `found` | boolean | Whether an email was found |
|
||||||
|
| `status` | string | Request status |
|
||||||
|
|
||||||
|
### `enrich_phone_finder`
|
||||||
|
|
||||||
|
Find a phone number from a LinkedIn profile URL.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `linkedinProfile` | string | Yes | LinkedIn profile URL \(e.g., linkedin.com/in/williamhgates\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `profileUrl` | string | LinkedIn profile URL |
|
||||||
|
| `mobileNumber` | string | Found mobile phone number |
|
||||||
|
| `found` | boolean | Whether a phone number was found |
|
||||||
|
| `status` | string | Request status \(in_progress or completed\) |
|
||||||
|
|
||||||
|
### `enrich_email_to_phone`
|
||||||
|
|
||||||
|
Find a phone number associated with an email address.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `email` | string | Yes | Email address to look up \(e.g., john.doe@example.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `email` | string | Email address looked up |
|
||||||
|
| `mobileNumber` | string | Found mobile phone number |
|
||||||
|
| `found` | boolean | Whether a phone number was found |
|
||||||
|
| `status` | string | Request status \(in_progress or completed\) |
|
||||||
|
|
||||||
|
### `enrich_verify_email`
|
||||||
|
|
||||||
|
Verify an email address for deliverability, including catch-all detection and provider identification.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `email` | string | Yes | Email address to verify \(e.g., john.doe@example.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `email` | string | Email address verified |
|
||||||
|
| `status` | string | Verification status |
|
||||||
|
| `result` | string | Deliverability result \(deliverable, undeliverable, etc.\) |
|
||||||
|
| `confidenceScore` | number | Confidence score \(0-100\) |
|
||||||
|
| `smtpProvider` | string | Email service provider \(e.g., Google, Microsoft\) |
|
||||||
|
| `mailDisposable` | boolean | Whether the email is from a disposable provider |
|
||||||
|
| `mailAcceptAll` | boolean | Whether the domain is a catch-all domain |
|
||||||
|
| `free` | boolean | Whether the email uses a free email service |
|
||||||
|
|
||||||
|
### `enrich_disposable_email_check`
|
||||||
|
|
||||||
|
Check if an email address is from a disposable or temporary email provider. Returns a score and validation details.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `email` | string | Yes | Email address to check \(e.g., john.doe@example.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `email` | string | Email address checked |
|
||||||
|
| `score` | number | Validation score \(0-100\) |
|
||||||
|
| `testsPassed` | string | Number of tests passed \(e.g., "3/3"\) |
|
||||||
|
| `passed` | boolean | Whether the email passed all validation tests |
|
||||||
|
| `reason` | string | Reason for failure if email did not pass |
|
||||||
|
| `mailServerIp` | string | Mail server IP address |
|
||||||
|
| `mxRecords` | array | MX records for the domain |
|
||||||
|
| ↳ `host` | string | MX record host |
|
||||||
|
| ↳ `pref` | number | MX record preference |
|
||||||
|
|
||||||
|
### `enrich_email_to_ip`
|
||||||
|
|
||||||
|
Discover an IP address associated with an email address.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `email` | string | Yes | Email address to look up \(e.g., john.doe@example.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `email` | string | Email address looked up |
|
||||||
|
| `ip` | string | Associated IP address |
|
||||||
|
| `found` | boolean | Whether an IP address was found |
|
||||||
|
|
||||||
|
### `enrich_ip_to_company`
|
||||||
|
|
||||||
|
Identify a company from an IP address with detailed firmographic information.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `ip` | string | Yes | IP address to look up \(e.g., 86.92.60.221\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `name` | string | Company name |
|
||||||
|
| `legalName` | string | Legal company name |
|
||||||
|
| `domain` | string | Primary domain |
|
||||||
|
| `domainAliases` | array | Domain aliases |
|
||||||
|
| `sector` | string | Business sector |
|
||||||
|
| `industry` | string | Industry |
|
||||||
|
| `phone` | string | Phone number |
|
||||||
|
| `employees` | number | Number of employees |
|
||||||
|
| `revenue` | string | Estimated revenue |
|
||||||
|
| `location` | json | Company location |
|
||||||
|
| ↳ `city` | string | City |
|
||||||
|
| ↳ `state` | string | State |
|
||||||
|
| ↳ `country` | string | Country |
|
||||||
|
| ↳ `timezone` | string | Timezone |
|
||||||
|
| `linkedInUrl` | string | LinkedIn company URL |
|
||||||
|
| `twitterUrl` | string | Twitter URL |
|
||||||
|
| `facebookUrl` | string | Facebook URL |
|
||||||
|
|
||||||
|
### `enrich_company_lookup`
|
||||||
|
|
||||||
|
Look up comprehensive company information by name or domain including funding, location, and social profiles.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `name` | string | No | Company name \(e.g., Google\) |
|
||||||
|
| `domain` | string | No | Company domain \(e.g., google.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `name` | string | Company name |
|
||||||
|
| `universalName` | string | Universal company name |
|
||||||
|
| `companyId` | string | Company ID |
|
||||||
|
| `description` | string | Company description |
|
||||||
|
| `phone` | string | Phone number |
|
||||||
|
| `linkedInUrl` | string | LinkedIn company URL |
|
||||||
|
| `websiteUrl` | string | Company website |
|
||||||
|
| `followers` | number | Number of LinkedIn followers |
|
||||||
|
| `staffCount` | number | Number of employees |
|
||||||
|
| `foundedDate` | string | Date founded |
|
||||||
|
| `type` | string | Company type |
|
||||||
|
| `industries` | array | Industries |
|
||||||
|
| `specialties` | array | Company specialties |
|
||||||
|
| `headquarters` | json | Headquarters location |
|
||||||
|
| ↳ `city` | string | City |
|
||||||
|
| ↳ `country` | string | Country |
|
||||||
|
| ↳ `postalCode` | string | Postal code |
|
||||||
|
| ↳ `line1` | string | Address line 1 |
|
||||||
|
| `logo` | string | Company logo URL |
|
||||||
|
| `coverImage` | string | Cover image URL |
|
||||||
|
| `fundingRounds` | array | Funding history |
|
||||||
|
| ↳ `roundType` | string | Funding round type |
|
||||||
|
| ↳ `amount` | number | Amount raised |
|
||||||
|
| ↳ `currency` | string | Currency |
|
||||||
|
| ↳ `investors` | array | Investors |
|
||||||
|
|
||||||
|
### `enrich_company_funding`
|
||||||
|
|
||||||
|
Retrieve company funding history, traffic metrics, and executive information by domain.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `domain` | string | Yes | Company domain \(e.g., example.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `legalName` | string | Legal company name |
|
||||||
|
| `employeeCount` | number | Number of employees |
|
||||||
|
| `headquarters` | string | Headquarters location |
|
||||||
|
| `industry` | string | Industry |
|
||||||
|
| `totalFundingRaised` | number | Total funding raised |
|
||||||
|
| `fundingRounds` | array | Funding rounds |
|
||||||
|
| ↳ `roundType` | string | Round type |
|
||||||
|
| ↳ `amount` | number | Amount raised |
|
||||||
|
| ↳ `date` | string | Date |
|
||||||
|
| ↳ `investors` | array | Investors |
|
||||||
|
| `monthlyVisits` | number | Monthly website visits |
|
||||||
|
| `trafficChange` | number | Traffic change percentage |
|
||||||
|
| `itSpending` | number | Estimated IT spending in USD |
|
||||||
|
| `executives` | array | Executive team |
|
||||||
|
| ↳ `name` | string | Name |
|
||||||
|
| ↳ `title` | string | Title |
|
||||||
|
|
||||||
|
### `enrich_company_revenue`
|
||||||
|
|
||||||
|
Retrieve company revenue data, CEO information, and competitive analysis by domain.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `domain` | string | Yes | Company domain \(e.g., clay.io\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `companyName` | string | Company name |
|
||||||
|
| `shortDescription` | string | Short company description |
|
||||||
|
| `fullSummary` | string | Full company summary |
|
||||||
|
| `revenue` | string | Company revenue |
|
||||||
|
| `revenueMin` | number | Minimum revenue estimate |
|
||||||
|
| `revenueMax` | number | Maximum revenue estimate |
|
||||||
|
| `employeeCount` | number | Number of employees |
|
||||||
|
| `founded` | string | Year founded |
|
||||||
|
| `ownership` | string | Ownership type |
|
||||||
|
| `status` | string | Company status \(e.g., Active\) |
|
||||||
|
| `website` | string | Company website URL |
|
||||||
|
| `ceo` | json | CEO information |
|
||||||
|
| ↳ `name` | string | CEO name |
|
||||||
|
| ↳ `designation` | string | CEO designation/title |
|
||||||
|
| ↳ `rating` | number | CEO rating |
|
||||||
|
| `socialLinks` | json | Social media links |
|
||||||
|
| ↳ `linkedIn` | string | LinkedIn URL |
|
||||||
|
| ↳ `twitter` | string | Twitter URL |
|
||||||
|
| ↳ `facebook` | string | Facebook URL |
|
||||||
|
| `totalFunding` | string | Total funding raised |
|
||||||
|
| `fundingRounds` | number | Number of funding rounds |
|
||||||
|
| `competitors` | array | Competitors |
|
||||||
|
| ↳ `name` | string | Competitor name |
|
||||||
|
| ↳ `revenue` | string | Revenue |
|
||||||
|
| ↳ `employeeCount` | number | Employee count |
|
||||||
|
| ↳ `headquarters` | string | Headquarters |
|
||||||
|
|
||||||
|
### `enrich_search_people`
|
||||||
|
|
||||||
|
Search for professionals by various criteria including name, title, skills, education, and company.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `firstName` | string | No | First name |
|
||||||
|
| `lastName` | string | No | Last name |
|
||||||
|
| `summary` | string | No | Professional summary keywords |
|
||||||
|
| `subTitle` | string | No | Job title/subtitle |
|
||||||
|
| `locationCountry` | string | No | Country |
|
||||||
|
| `locationCity` | string | No | City |
|
||||||
|
| `locationState` | string | No | State/province |
|
||||||
|
| `influencer` | boolean | No | Filter for influencers only |
|
||||||
|
| `premium` | boolean | No | Filter for premium accounts only |
|
||||||
|
| `language` | string | No | Primary language |
|
||||||
|
| `industry` | string | No | Industry |
|
||||||
|
| `currentJobTitles` | json | No | Current job titles \(array\) |
|
||||||
|
| `pastJobTitles` | json | No | Past job titles \(array\) |
|
||||||
|
| `skills` | json | No | Skills to search for \(array\) |
|
||||||
|
| `schoolNames` | json | No | School names \(array\) |
|
||||||
|
| `certifications` | json | No | Certifications to filter by \(array\) |
|
||||||
|
| `degreeNames` | json | No | Degree names to filter by \(array\) |
|
||||||
|
| `studyFields` | json | No | Fields of study to filter by \(array\) |
|
||||||
|
| `currentCompanies` | json | No | Current company IDs to filter by \(array of numbers\) |
|
||||||
|
| `pastCompanies` | json | No | Past company IDs to filter by \(array of numbers\) |
|
||||||
|
| `currentPage` | number | No | Page number \(default: 1\) |
|
||||||
|
| `pageSize` | number | No | Results per page \(default: 20\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `currentPage` | number | Current page number |
|
||||||
|
| `totalPage` | number | Total number of pages |
|
||||||
|
| `pageSize` | number | Results per page |
|
||||||
|
| `profiles` | array | Search results |
|
||||||
|
| ↳ `profileIdentifier` | string | Profile ID |
|
||||||
|
| ↳ `givenName` | string | First name |
|
||||||
|
| ↳ `familyName` | string | Last name |
|
||||||
|
| ↳ `currentPosition` | string | Current job title |
|
||||||
|
| ↳ `profileImage` | string | Profile image URL |
|
||||||
|
| ↳ `externalProfileUrl` | string | LinkedIn URL |
|
||||||
|
| ↳ `city` | string | City |
|
||||||
|
| ↳ `country` | string | Country |
|
||||||
|
| ↳ `expertSkills` | array | Skills |
|
||||||
|
|
||||||
|
### `enrich_search_company`
|
||||||
|
|
||||||
|
Search for companies by various criteria including name, industry, location, and size.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `name` | string | No | Company name |
|
||||||
|
| `website` | string | No | Company website URL |
|
||||||
|
| `tagline` | string | No | Company tagline |
|
||||||
|
| `type` | string | No | Company type \(e.g., Private, Public\) |
|
||||||
|
| `description` | string | No | Company description keywords |
|
||||||
|
| `industries` | json | No | Industries to filter by \(array\) |
|
||||||
|
| `locationCountry` | string | No | Country |
|
||||||
|
| `locationCity` | string | No | City |
|
||||||
|
| `postalCode` | string | No | Postal code |
|
||||||
|
| `locationCountryList` | json | No | Multiple countries to filter by \(array\) |
|
||||||
|
| `locationCityList` | json | No | Multiple cities to filter by \(array\) |
|
||||||
|
| `specialities` | json | No | Company specialties \(array\) |
|
||||||
|
| `followers` | number | No | Minimum number of followers |
|
||||||
|
| `staffCount` | number | No | Maximum staff count |
|
||||||
|
| `staffCountMin` | number | No | Minimum staff count |
|
||||||
|
| `staffCountMax` | number | No | Maximum staff count |
|
||||||
|
| `currentPage` | number | No | Page number \(default: 1\) |
|
||||||
|
| `pageSize` | number | No | Results per page \(default: 20\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `currentPage` | number | Current page number |
|
||||||
|
| `totalPage` | number | Total number of pages |
|
||||||
|
| `pageSize` | number | Results per page |
|
||||||
|
| `companies` | array | Search results |
|
||||||
|
| ↳ `companyName` | string | Company name |
|
||||||
|
| ↳ `tagline` | string | Company tagline |
|
||||||
|
| ↳ `webAddress` | string | Website URL |
|
||||||
|
| ↳ `industries` | array | Industries |
|
||||||
|
| ↳ `teamSize` | number | Team size |
|
||||||
|
| ↳ `linkedInProfile` | string | LinkedIn URL |
|
||||||
|
|
||||||
|
### `enrich_search_company_employees`
|
||||||
|
|
||||||
|
Search for employees within specific companies by location and job title.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `companyIds` | json | No | Array of company IDs to search within |
|
||||||
|
| `country` | string | No | Country filter \(e.g., United States\) |
|
||||||
|
| `city` | string | No | City filter \(e.g., San Francisco\) |
|
||||||
|
| `state` | string | No | State filter \(e.g., California\) |
|
||||||
|
| `jobTitles` | json | No | Job titles to filter by \(array\) |
|
||||||
|
| `page` | number | No | Page number \(default: 1\) |
|
||||||
|
| `pageSize` | number | No | Results per page \(default: 10\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `currentPage` | number | Current page number |
|
||||||
|
| `totalPage` | number | Total number of pages |
|
||||||
|
| `pageSize` | number | Number of results per page |
|
||||||
|
| `profiles` | array | Employee profiles |
|
||||||
|
| ↳ `profileIdentifier` | string | Profile ID |
|
||||||
|
| ↳ `givenName` | string | First name |
|
||||||
|
| ↳ `familyName` | string | Last name |
|
||||||
|
| ↳ `currentPosition` | string | Current job title |
|
||||||
|
| ↳ `profileImage` | string | Profile image URL |
|
||||||
|
| ↳ `externalProfileUrl` | string | LinkedIn URL |
|
||||||
|
| ↳ `city` | string | City |
|
||||||
|
| ↳ `country` | string | Country |
|
||||||
|
| ↳ `expertSkills` | array | Skills |
|
||||||
|
|
||||||
|
### `enrich_search_similar_companies`
|
||||||
|
|
||||||
|
Find companies similar to a given company by LinkedIn URL with filters for location and size.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `url` | string | Yes | LinkedIn company URL \(e.g., linkedin.com/company/google\) |
|
||||||
|
| `accountLocation` | json | No | Filter by locations \(array of country names\) |
|
||||||
|
| `employeeSizeType` | string | No | Employee size filter type \(e.g., RANGE\) |
|
||||||
|
| `employeeSizeRange` | json | No | Employee size ranges \(array of \{start, end\} objects\) |
|
||||||
|
| `page` | number | No | Page number \(default: 1\) |
|
||||||
|
| `num` | number | No | Number of results per page |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `companies` | array | Similar companies |
|
||||||
|
| ↳ `url` | string | LinkedIn URL |
|
||||||
|
| ↳ `name` | string | Company name |
|
||||||
|
| ↳ `universalName` | string | Universal name |
|
||||||
|
| ↳ `type` | string | Company type |
|
||||||
|
| ↳ `description` | string | Description |
|
||||||
|
| ↳ `phone` | string | Phone number |
|
||||||
|
| ↳ `website` | string | Website URL |
|
||||||
|
| ↳ `logo` | string | Logo URL |
|
||||||
|
| ↳ `foundedYear` | number | Year founded |
|
||||||
|
| ↳ `staffTotal` | number | Total staff |
|
||||||
|
| ↳ `industries` | array | Industries |
|
||||||
|
| ↳ `relevancyScore` | number | Relevancy score |
|
||||||
|
| ↳ `relevancyValue` | string | Relevancy value |
|
||||||
|
|
||||||
|
### `enrich_sales_pointer_people`
|
||||||
|
|
||||||
|
Advanced people search with complex filters for location, company size, seniority, experience, and more.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `page` | number | Yes | Page number \(starts at 1\) |
|
||||||
|
| `filters` | json | Yes | Array of filter objects. Each filter has type \(e.g., POSTAL_CODE, COMPANY_HEADCOUNT\), values \(array with id, text, selectionType: INCLUDED/EXCLUDED\), and optional selectedSubFilter |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `data` | array | People results |
|
||||||
|
| ↳ `name` | string | Full name |
|
||||||
|
| ↳ `summary` | string | Professional summary |
|
||||||
|
| ↳ `location` | string | Location |
|
||||||
|
| ↳ `profilePicture` | string | Profile picture URL |
|
||||||
|
| ↳ `linkedInUrn` | string | LinkedIn URN |
|
||||||
|
| ↳ `positions` | array | Work positions |
|
||||||
|
| ↳ `education` | array | Education |
|
||||||
|
| `pagination` | json | Pagination info |
|
||||||
|
| ↳ `totalCount` | number | Total results |
|
||||||
|
| ↳ `returnedCount` | number | Returned count |
|
||||||
|
| ↳ `start` | number | Start position |
|
||||||
|
| ↳ `limit` | number | Limit |
|
||||||
|
|
||||||
|
### `enrich_search_posts`
|
||||||
|
|
||||||
|
Search LinkedIn posts by keywords with date filtering.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `keywords` | string | Yes | Search keywords \(e.g., "AI automation"\) |
|
||||||
|
| `datePosted` | string | No | Time filter \(e.g., past_week, past_month\) |
|
||||||
|
| `page` | number | No | Page number \(default: 1\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `count` | number | Total number of results |
|
||||||
|
| `posts` | array | Search results |
|
||||||
|
| ↳ `url` | string | Post URL |
|
||||||
|
| ↳ `postId` | string | Post ID |
|
||||||
|
| ↳ `author` | object | Author information |
|
||||||
|
| ↳ `name` | string | Author name |
|
||||||
|
| ↳ `headline` | string | Author headline |
|
||||||
|
| ↳ `linkedInUrl` | string | Author LinkedIn URL |
|
||||||
|
| ↳ `profileImage` | string | Author profile image |
|
||||||
|
| ↳ `timestamp` | string | Post timestamp |
|
||||||
|
| ↳ `textContent` | string | Post text content |
|
||||||
|
| ↳ `hashtags` | array | Hashtags |
|
||||||
|
| ↳ `mediaUrls` | array | Media URLs |
|
||||||
|
| ↳ `reactions` | number | Number of reactions |
|
||||||
|
| ↳ `commentsCount` | number | Number of comments |
|
||||||
|
|
||||||
|
### `enrich_get_post_details`
|
||||||
|
|
||||||
|
Get detailed information about a LinkedIn post by URL.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `url` | string | Yes | LinkedIn post URL |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `postId` | string | Post ID |
|
||||||
|
| `author` | json | Author information |
|
||||||
|
| ↳ `name` | string | Author name |
|
||||||
|
| ↳ `headline` | string | Author headline |
|
||||||
|
| ↳ `linkedInUrl` | string | Author LinkedIn URL |
|
||||||
|
| ↳ `profileImage` | string | Author profile image |
|
||||||
|
| `timestamp` | string | Post timestamp |
|
||||||
|
| `textContent` | string | Post text content |
|
||||||
|
| `hashtags` | array | Hashtags |
|
||||||
|
| `mediaUrls` | array | Media URLs |
|
||||||
|
| `reactions` | number | Number of reactions |
|
||||||
|
| `commentsCount` | number | Number of comments |
|
||||||
|
|
||||||
|
### `enrich_search_post_reactions`
|
||||||
|
|
||||||
|
Get reactions on a LinkedIn post with filtering by reaction type.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `postUrn` | string | Yes | LinkedIn activity URN \(e.g., urn:li:activity:7231931952839196672\) |
|
||||||
|
| `reactionType` | string | Yes | Reaction type filter: all, like, love, celebrate, insightful, or funny \(default: all\) |
|
||||||
|
| `page` | number | Yes | Page number \(starts at 1\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `page` | number | Current page number |
|
||||||
|
| `totalPage` | number | Total number of pages |
|
||||||
|
| `count` | number | Number of reactions returned |
|
||||||
|
| `reactions` | array | Reactions |
|
||||||
|
| ↳ `reactionType` | string | Type of reaction |
|
||||||
|
| ↳ `reactor` | object | Person who reacted |
|
||||||
|
| ↳ `name` | string | Name |
|
||||||
|
| ↳ `subTitle` | string | Job title |
|
||||||
|
| ↳ `profileId` | string | Profile ID |
|
||||||
|
| ↳ `profilePicture` | string | Profile picture URL |
|
||||||
|
| ↳ `linkedInUrl` | string | LinkedIn URL |
|
||||||
|
|
||||||
|
### `enrich_search_post_comments`
|
||||||
|
|
||||||
|
Get comments on a LinkedIn post.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `postUrn` | string | Yes | LinkedIn activity URN \(e.g., urn:li:activity:7191163324208705536\) |
|
||||||
|
| `page` | number | No | Page number \(starts at 1, default: 1\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `page` | number | Current page number |
|
||||||
|
| `totalPage` | number | Total number of pages |
|
||||||
|
| `count` | number | Number of comments returned |
|
||||||
|
| `comments` | array | Comments |
|
||||||
|
| ↳ `activityId` | string | Comment activity ID |
|
||||||
|
| ↳ `commentary` | string | Comment text |
|
||||||
|
| ↳ `linkedInUrl` | string | Link to comment |
|
||||||
|
| ↳ `commenter` | object | Commenter info |
|
||||||
|
| ↳ `profileId` | string | Profile ID |
|
||||||
|
| ↳ `firstName` | string | First name |
|
||||||
|
| ↳ `lastName` | string | Last name |
|
||||||
|
| ↳ `subTitle` | string | Subtitle/headline |
|
||||||
|
| ↳ `profilePicture` | string | Profile picture URL |
|
||||||
|
| ↳ `backgroundImage` | string | Background image URL |
|
||||||
|
| ↳ `entityUrn` | string | Entity URN |
|
||||||
|
| ↳ `objectUrn` | string | Object URN |
|
||||||
|
| ↳ `profileType` | string | Profile type |
|
||||||
|
| ↳ `reactionBreakdown` | object | Reactions on the comment |
|
||||||
|
| ↳ `likes` | number | Number of likes |
|
||||||
|
| ↳ `empathy` | number | Number of empathy reactions |
|
||||||
|
| ↳ `other` | number | Number of other reactions |
|
||||||
|
|
||||||
|
### `enrich_search_people_activities`
|
||||||
|
|
||||||
|
Get a person
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `profileId` | string | Yes | LinkedIn profile ID |
|
||||||
|
| `activityType` | string | Yes | Activity type: posts, comments, or articles |
|
||||||
|
| `paginationToken` | string | No | Pagination token for next page of results |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `paginationToken` | string | Token for fetching next page |
|
||||||
|
| `activityType` | string | Type of activities returned |
|
||||||
|
| `activities` | array | Activities |
|
||||||
|
| ↳ `activityId` | string | Activity ID |
|
||||||
|
| ↳ `commentary` | string | Activity text content |
|
||||||
|
| ↳ `linkedInUrl` | string | Link to activity |
|
||||||
|
| ↳ `timeElapsed` | string | Time elapsed since activity |
|
||||||
|
| ↳ `numReactions` | number | Total number of reactions |
|
||||||
|
| ↳ `author` | object | Activity author info |
|
||||||
|
| ↳ `name` | string | Author name |
|
||||||
|
| ↳ `profileId` | string | Profile ID |
|
||||||
|
| ↳ `profilePicture` | string | Profile picture URL |
|
||||||
|
| ↳ `reactionBreakdown` | object | Reactions |
|
||||||
|
| ↳ `likes` | number | Likes |
|
||||||
|
| ↳ `empathy` | number | Empathy reactions |
|
||||||
|
| ↳ `other` | number | Other reactions |
|
||||||
|
| ↳ `attachments` | array | Attachment URLs |
|
||||||
|
|
||||||
|
### `enrich_search_company_activities`
|
||||||
|
|
||||||
|
Get a company
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `companyId` | string | Yes | LinkedIn company ID |
|
||||||
|
| `activityType` | string | Yes | Activity type: posts, comments, or articles |
|
||||||
|
| `paginationToken` | string | No | Pagination token for next page of results |
|
||||||
|
| `offset` | number | No | Number of records to skip \(default: 0\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `paginationToken` | string | Token for fetching next page |
|
||||||
|
| `activityType` | string | Type of activities returned |
|
||||||
|
| `activities` | array | Activities |
|
||||||
|
| ↳ `activityId` | string | Activity ID |
|
||||||
|
| ↳ `commentary` | string | Activity text content |
|
||||||
|
| ↳ `linkedInUrl` | string | Link to activity |
|
||||||
|
| ↳ `timeElapsed` | string | Time elapsed since activity |
|
||||||
|
| ↳ `numReactions` | number | Total number of reactions |
|
||||||
|
| ↳ `author` | object | Activity author info |
|
||||||
|
| ↳ `name` | string | Author name |
|
||||||
|
| ↳ `profileId` | string | Profile ID |
|
||||||
|
| ↳ `profilePicture` | string | Profile picture URL |
|
||||||
|
| ↳ `reactionBreakdown` | object | Reactions |
|
||||||
|
| ↳ `likes` | number | Likes |
|
||||||
|
| ↳ `empathy` | number | Empathy reactions |
|
||||||
|
| ↳ `other` | number | Other reactions |
|
||||||
|
| ↳ `attachments` | array | Attachments |
|
||||||
|
|
||||||
|
### `enrich_reverse_hash_lookup`
|
||||||
|
|
||||||
|
Convert an MD5 email hash back to the original email address and display name.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `hash` | string | Yes | MD5 hash value to look up |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `hash` | string | MD5 hash that was looked up |
|
||||||
|
| `email` | string | Original email address |
|
||||||
|
| `displayName` | string | Display name associated with the email |
|
||||||
|
| `found` | boolean | Whether an email was found for the hash |
|
||||||
|
|
||||||
|
### `enrich_search_logo`
|
||||||
|
|
||||||
|
Get a company logo image URL by domain.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `apiKey` | string | Yes | Enrich API key |
|
||||||
|
| `url` | string | Yes | Company domain \(e.g., google.com\) |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `logoUrl` | string | URL to fetch the company logo |
|
||||||
|
| `domain` | string | Domain that was looked up |
|
||||||
|
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ description: Read and parse multiple files
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="file_v2"
|
type="file_v3"
|
||||||
color="#40916C"
|
color="#40916C"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
|
|||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|
||||||
Integrate File into the workflow. Can upload a file manually or insert a file url.
|
Upload files directly or import from external URLs to get UserFile objects for use in other blocks.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -41,14 +41,15 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
|
|||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `filePath` | string | Yes | Path to the file\(s\). Can be a single path, URL, or an array of paths. |
|
| `filePath` | string | No | Path to the file\(s\). Can be a single path, URL, or an array of paths. |
|
||||||
|
| `file` | file | No | Uploaded file\(s\) to parse |
|
||||||
| `fileType` | string | No | Type of file to parse \(auto-detected if not specified\) |
|
| `fileType` | string | No | Type of file to parse \(auto-detected if not specified\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `files` | array | Array of parsed files with content, metadata, and file properties |
|
| `files` | file[] | Parsed files as UserFile objects |
|
||||||
| `combinedContent` | string | All file contents merged into a single text string |
|
| `combinedContent` | string | Combined content of all parsed files |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Interact with Fireflies.ai meeting transcripts and recordings
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="fireflies"
|
type="fireflies_v2"
|
||||||
color="#100730"
|
color="#100730"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,23 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
color="#181C1E"
|
color="#181C1E"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
|
[GitHub](https://github.com/) is the world’s leading platform for hosting, collaborating on, and managing source code. GitHub offers powerful tools for version control, code review, branching strategies, and team collaboration within the rich Git ecosystem, underpinning both open source and enterprise development worldwide.
|
||||||
|
|
||||||
|
The GitHub integration in Sim allows your agents to seamlessly automate, interact with, and orchestrate workflows across your repositories. Using this integration, agents can perform an extended set of code and collaboration operations, enabling:
|
||||||
|
|
||||||
|
- **Fetch pull request details:** Retrieve a full overview of any pull request, including file diffs, branch information, metadata, approvals, and a summary of changes, for automation or review workflows.
|
||||||
|
- **Create pull request comments:** Automatically generate or post comments on PRs—such as reviews, suggestions, or status updates—enabling speedy feedback, documentation, or policy enforcement.
|
||||||
|
- **Get repository information:** Access comprehensive repository metadata, including descriptions, visibility, topics, default branches, and contributors. This supports intelligent project analysis, dynamic workflow routing, and organizational reporting.
|
||||||
|
- **Fetch the latest commit:** Quickly obtain details from the newest commit on any branch, including hashes, messages, authors, and timestamps. This is useful for monitoring development velocity, triggering downstream actions, or enforcing quality checks.
|
||||||
|
- **Trigger workflows from GitHub events:** Set up Sim workflows to start automatically from key GitHub events, including pull request creation, review comments, or when new commits are pushed, through easy webhook integration. Automate actions such as deployments, notifications, compliance checks, or documentation updates in real time.
|
||||||
|
- **Monitor and manage repository activity:** Programmatically track contributions, manage PR review states, analyze branch histories, and audit code changes. Empower agents to enforce requirements, coordinate releases, and respond dynamically to development patterns.
|
||||||
|
- **Support for advanced automations:** Combine these operations—for example, fetch PR data, leave context-aware comments, and kick off multi-step Sim workflows on code pushes or PR merges—to automate your team’s engineering processes from end to end.
|
||||||
|
|
||||||
|
By leveraging all of these capabilities, the Sim GitHub integration enables agents to engage deeply in the development lifecycle. Automate code reviews, streamline team feedback, synchronize project artifacts, accelerate CI/CD, and enforce best practices with ease. Bring security, speed, and reliability to your workflows—directly within your Sim-powered automation environment, with full integration into your organization’s GitHub strategy.
|
||||||
|
{/* MANUAL-CONTENT-END */}
|
||||||
|
|
||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|
||||||
Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed.
|
Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed.
|
||||||
@@ -675,6 +692,7 @@ Get the content of a file from a GitHub repository. Supports files up to 1MB. Co
|
|||||||
| `download_url` | string | Direct download URL |
|
| `download_url` | string | Direct download URL |
|
||||||
| `git_url` | string | Git blob API URL |
|
| `git_url` | string | Git blob API URL |
|
||||||
| `_links` | json | Related links |
|
| `_links` | json | Related links |
|
||||||
|
| `file` | file | Downloaded file stored in execution files |
|
||||||
|
|
||||||
### `github_create_file`
|
### `github_create_file`
|
||||||
|
|
||||||
|
|||||||
@@ -11,55 +11,17 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* MANUAL-CONTENT-START:intro */}
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
[Google Docs](https://docs.google.com) is a powerful cloud-based document creation and editing service that allows users to create, edit, and collaborate on documents in real-time. As part of Google's productivity suite, Google Docs offers a versatile platform for text documents with robust formatting, commenting, and sharing capabilities.
|
[Google Docs](https://docs.google.com) is Google’s collaborative, cloud-based document service, enabling users to create, edit, and share documents in real time. As an integral part of Google Workspace, Docs offers rich formatting tools, commenting, version history, and seamless integration with other Google productivity tools.
|
||||||
|
|
||||||
Learn how to integrate the Google Docs "Read" tool in Sim to effortlessly fetch data from your docs and to integrate into your workflows. This tutorial walks you through connecting Google Docs, setting up data reads, and using that information to automate processes in real-time. Perfect for syncing live data with your agents.
|
Google Docs empowers individuals and teams to:
|
||||||
|
|
||||||
<iframe
|
- **Create and format documents:** Develop rich text documents with advanced formatting, images, and tables.
|
||||||
width="100%"
|
- **Collaborate and comment:** Multiple users can edit and comment with suggestions instantly.
|
||||||
height="400"
|
- **Track changes and version history:** Review, revert, and manage revisions over time.
|
||||||
src="https://www.youtube.com/embed/f41gy9rBHhE"
|
- **Access from any device:** Work on documents from web, mobile, or desktop with full cloud synchronization.
|
||||||
title="Use the Google Docs Read tool in Sim"
|
- **Integrate across Google services:** Connect Docs with Drive, Sheets, Slides, and external platforms for powerful workflows.
|
||||||
frameBorder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
Learn how to integrate the Google Docs "Update" tool in Sim to effortlessly add content in your docs through your workflows. This tutorial walks you through connecting Google Docs, configuring data writes, and using that information to automate document updates seamlessly. Perfect for maintaining dynamic, real-time documentation with minimal effort.
|
In Sim, the Google Docs integration allows your agents to read document content, write new content, and create documents programmatically as part of automated workflows. This integration unlocks automation such as document generation, report writing, content extraction, and collaborative editing—bridging the gap between AI-driven workflows and document management in your organization.
|
||||||
|
|
||||||
<iframe
|
|
||||||
width="100%"
|
|
||||||
height="400"
|
|
||||||
src="https://www.youtube.com/embed/L64ROHS2ivA"
|
|
||||||
title="Use the Google Docs Update tool in Sim"
|
|
||||||
frameBorder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
Learn how to integrate the Google Docs "Create" tool in Sim to effortlessly generate new documents through your workflows. This tutorial walks you through connecting Google Docs, setting up document creation, and using workflow data to populate content automatically. Perfect for streamlining document generation and enhancing productivity.
|
|
||||||
|
|
||||||
<iframe
|
|
||||||
width="100%"
|
|
||||||
height="400"
|
|
||||||
src="https://www.youtube.com/embed/lWpHH4qddWk"
|
|
||||||
title="Use the Google Docs Create tool in Sim"
|
|
||||||
frameBorder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
With Google Docs, you can:
|
|
||||||
|
|
||||||
- **Create and edit documents**: Develop text documents with comprehensive formatting options
|
|
||||||
- **Collaborate in real-time**: Work simultaneously with multiple users on the same document
|
|
||||||
- **Track changes**: View revision history and restore previous versions
|
|
||||||
- **Comment and suggest**: Provide feedback and propose edits without changing the original content
|
|
||||||
- **Access anywhere**: Use Google Docs across devices with automatic cloud synchronization
|
|
||||||
- **Work offline**: Continue working without internet connection with changes syncing when back online
|
|
||||||
- **Integrate with other services**: Connect with Google Drive, Sheets, Slides, and third-party applications
|
|
||||||
|
|
||||||
In Sim, the Google Docs integration enables your agents to interact directly with document content programmatically. This allows for powerful automation scenarios such as document creation, content extraction, collaborative editing, and document management. Your agents can read existing documents to extract information, write to documents to update content, and create new documents from scratch. This integration bridges the gap between your AI workflows and document management, enabling seamless interaction with one of the world's most widely used document platforms. By connecting Sim with Google Docs, you can automate document workflows, generate reports, extract insights from documents, and maintain documentation - all through your intelligent agents.
|
|
||||||
{/* MANUAL-CONTENT-END */}
|
{/* MANUAL-CONTENT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,30 +11,18 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* MANUAL-CONTENT-START:intro */}
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
[Google Drive](https://drive.google.com) is Google's cloud storage and file synchronization service that allows users to store files, synchronize files across devices, and share files with others. As a core component of Google's productivity ecosystem, Google Drive offers robust storage, organization, and collaboration capabilities.
|
[Google Drive](https://drive.google.com) is Google’s cloud-based file storage and synchronization service, making it easy to store, manage, share, and access files securely across devices and platforms. As a core element of Google Workspace, Google Drive offers robust tools for file organization, collaboration, and seamless integration with the broader productivity suite.
|
||||||
|
|
||||||
Learn how to integrate the Google Drive tool in Sim to effortlessly pull information from your Drive through your workflows. This tutorial walks you through connecting Google Drive, setting up data retrieval, and using stored documents and files to enhance automation. Perfect for syncing important data with your agents in real-time.
|
Google Drive enables individuals and teams to:
|
||||||
|
|
||||||
<iframe
|
- **Store files in the cloud:** Access documents, images, videos, and more from anywhere with internet connectivity.
|
||||||
width="100%"
|
- **Organize and manage content:** Create and arrange folders, use naming conventions, and leverage search for fast retrieval.
|
||||||
height="400"
|
- **Share and collaborate:** Control file and folder permissions, share with individuals or groups, and collaborate in real time.
|
||||||
src="https://www.youtube.com/embed/cRoRr4b-EAs"
|
- **Leverage powerful search:** Quickly locate files using Google’s search technology.
|
||||||
title="Use the Google Drive tool in Sim"
|
- **Access across devices:** Work with your files on desktop, mobile, or web with full synchronization.
|
||||||
frameBorder="0"
|
- **Integrate deeply across Google services:** Connect with Google Docs, Sheets, Slides, and partner applications in your workflows.
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
With Google Drive, you can:
|
In Sim, the Google Drive integration allows your agents to read, upload, download, list, and organize your Drive files programmatically. Agents can automate file management, streamline content workflows, and enable no-code automation around document storage and retrieval. By connecting Sim with Google Drive, you empower your agents to incorporate cloud file operations directly into intelligent business processes.
|
||||||
|
|
||||||
- **Store files in the cloud**: Upload and access your files from anywhere with internet access
|
|
||||||
- **Organize content**: Create folders, use color coding, and implement naming conventions
|
|
||||||
- **Share and collaborate**: Control access permissions and work simultaneously on files
|
|
||||||
- **Search efficiently**: Find files quickly with Google's powerful search technology
|
|
||||||
- **Access across devices**: Use Google Drive on desktop, mobile, and web platforms
|
|
||||||
- **Integrate with other services**: Connect with Google Docs, Sheets, Slides, and third-party applications
|
|
||||||
|
|
||||||
In Sim, the Google Drive integration enables your agents to interact directly with your cloud storage programmatically. This allows for powerful automation scenarios such as file management, content organization, and document workflows. Your agents can upload new files to specific folders, download existing files to process their contents, and list folder contents to navigate your storage structure. This integration bridges the gap between your AI workflows and your document management system, enabling seamless file operations without manual intervention. By connecting Sim with Google Drive, you can automate file-based workflows, manage documents intelligently, and incorporate cloud storage operations into your agent's capabilities.
|
|
||||||
{/* MANUAL-CONTENT-END */}
|
{/* MANUAL-CONTENT-END */}
|
||||||
|
|
||||||
|
|
||||||
@@ -303,11 +291,7 @@ Download a file from Google Drive with complete metadata (exports Google Workspa
|
|||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `file` | object | Downloaded file data |
|
| `file` | file | Downloaded file stored in execution files |
|
||||||
| ↳ `name` | string | File name |
|
|
||||||
| ↳ `mimeType` | string | MIME type of the file |
|
|
||||||
| ↳ `data` | string | File content as base64-encoded string |
|
|
||||||
| ↳ `size` | number | File size in bytes |
|
|
||||||
| `metadata` | object | Complete file metadata from Google Drive |
|
| `metadata` | object | Complete file metadata from Google Drive |
|
||||||
| ↳ `id` | string | Google Drive file ID |
|
| ↳ `id` | string | Google Drive file ID |
|
||||||
| ↳ `kind` | string | Resource type identifier |
|
| ↳ `kind` | string | Resource type identifier |
|
||||||
|
|||||||
@@ -11,29 +11,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* MANUAL-CONTENT-START:intro */}
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
[Google Search](https://www.google.com) is the world's most widely used search engine, providing access to billions of web pages and information sources. Google Search uses sophisticated algorithms to deliver relevant search results based on user queries, making it an essential tool for finding information on the internet.
|
[Google Search](https://www.google.com) is the world's most widely used web search engine, making it easy to find information, discover new content, and answer questions in real time. With advanced search algorithms, Google Search helps you quickly locate web pages, images, news, and more using simple or complex queries.
|
||||||
|
|
||||||
Learn how to integrate the Google Search tool in Sim to effortlessly fetch real-time search results through your workflows. This tutorial walks you through connecting Google Search, configuring search queries, and using live data to enhance automation. Perfect for powering your agents with up-to-date information and smarter decision-making.
|
In Sim, the Google Search integration allows your agents to search the web and retrieve live information as part of automated workflows. This enables powerful use cases such as automated research, fact-checking, knowledge synthesis, and dynamic content discovery. By connecting Sim with Google Search, your agents can perform queries, process and analyze web results, and incorporate the latest information into their decisions—without manual effort. Enhance your workflows with always up-to-date knowledge from across the internet.
|
||||||
|
|
||||||
<iframe
|
|
||||||
width="100%"
|
|
||||||
height="400"
|
|
||||||
src="https://www.youtube.com/embed/1B7hV9b5UMQ"
|
|
||||||
title="Use the Google Search tool in Sim"
|
|
||||||
frameBorder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
With Google Search, you can:
|
|
||||||
|
|
||||||
- **Find relevant information**: Access billions of web pages with Google's powerful search algorithms
|
|
||||||
- **Get specific results**: Use search operators to refine and target your queries
|
|
||||||
- **Discover diverse content**: Find text, images, videos, news, and other content types
|
|
||||||
- **Access knowledge graphs**: Get structured information about people, places, and things
|
|
||||||
- **Utilize search features**: Take advantage of specialized search tools like calculators, unit converters, and more
|
|
||||||
|
|
||||||
In Sim, the Google Search integration enables your agents to search the web programmatically and incorporate search results into their workflows. This allows for powerful automation scenarios such as research, fact-checking, data gathering, and information synthesis. Your agents can formulate search queries, retrieve relevant results, and extract information from those results to make decisions or generate insights. This integration bridges the gap between your AI workflows and the vast information available on the web, enabling your agents to access up-to-date information from across the internet. By connecting Sim with Google Search, you can create agents that stay informed with the latest information, verify facts, conduct research, and provide users with relevant web content - all without leaving your workflow.
|
|
||||||
{/* MANUAL-CONTENT-END */}
|
{/* MANUAL-CONTENT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Read, write, and create presentations
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="google_slides"
|
type="google_slides_v2"
|
||||||
color="#E0E0E0"
|
color="#E0E0E0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -333,6 +333,28 @@ Get all attachments from a Jira issue
|
|||||||
| `issueKey` | string | Issue key |
|
| `issueKey` | string | Issue key |
|
||||||
| `attachments` | array | Array of attachments with id, filename, size, mimeType, created, author |
|
| `attachments` | array | Array of attachments with id, filename, size, mimeType, created, author |
|
||||||
|
|
||||||
|
### `jira_add_attachment`
|
||||||
|
|
||||||
|
Add attachments to a Jira issue
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||||
|
| `issueKey` | string | Yes | Jira issue key to add attachments to \(e.g., PROJ-123\) |
|
||||||
|
| `files` | file[] | Yes | Files to attach to the Jira issue |
|
||||||
|
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `ts` | string | Timestamp of the operation |
|
||||||
|
| `issueKey` | string | Issue key |
|
||||||
|
| `attachmentIds` | json | IDs of uploaded attachments |
|
||||||
|
| `files` | file[] | Uploaded attachment files |
|
||||||
|
|
||||||
### `jira_delete_attachment`
|
### `jira_delete_attachment`
|
||||||
|
|
||||||
Delete an attachment from a Jira issue
|
Delete an attachment from a Jira issue
|
||||||
|
|||||||
@@ -1022,7 +1022,8 @@ Add an attachment to an issue in Linear
|
|||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `issueId` | string | Yes | Issue ID to attach to |
|
| `issueId` | string | Yes | Issue ID to attach to |
|
||||||
| `url` | string | Yes | URL of the attachment |
|
| `url` | string | No | URL of the attachment |
|
||||||
|
| `file` | file | No | File to attach |
|
||||||
| `title` | string | Yes | Attachment title |
|
| `title` | string | Yes | Attachment title |
|
||||||
| `subtitle` | string | No | Attachment subtitle/description |
|
| `subtitle` | string | No | Attachment subtitle/description |
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
color="#F64F9E"
|
color="#F64F9E"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
|
The Memory tool enables your agents to store, retrieve, and manage conversation memories across workflows. It acts as a persistent memory store that agents can access to maintain conversation context, recall facts, or track actions over time.
|
||||||
|
|
||||||
|
With the Memory tool, you can:
|
||||||
|
|
||||||
|
- **Add new memories**: Store relevant information, events, or conversation history by saving agent or user messages into a structured memory database
|
||||||
|
- **Retrieve memories**: Fetch specific memories or all memories tied to a conversation, helping agents recall previous interactions or facts
|
||||||
|
- **Delete memories**: Remove outdated or incorrect memories from the database to maintain accurate context
|
||||||
|
- **Append to existing conversations**: Update or expand on existing memory threads by appending new messages with the same conversation identifier
|
||||||
|
|
||||||
|
Sim’s Memory block is especially useful for building agents that require persistent state—helping them remember what was said earlier in a conversation, persist facts between tasks, or apply long-term history in decision-making. By integrating Memory, you enable richer, more contextual, and more dynamic workflows for your agents.
|
||||||
|
{/* MANUAL-CONTENT-END */}
|
||||||
|
|
||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|
||||||
Integrate Memory into the workflow. Can add, get a memory, get all memories, and delete memories.
|
Integrate Memory into the workflow. Can add, get a memory, get all memories, and delete memories.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"dynamodb",
|
"dynamodb",
|
||||||
"elasticsearch",
|
"elasticsearch",
|
||||||
"elevenlabs",
|
"elevenlabs",
|
||||||
|
"enrich",
|
||||||
"exa",
|
"exa",
|
||||||
"file",
|
"file",
|
||||||
"firecrawl",
|
"firecrawl",
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ Write or update content in a Microsoft Teams chat
|
|||||||
| `createdTime` | string | Timestamp when message was created |
|
| `createdTime` | string | Timestamp when message was created |
|
||||||
| `url` | string | Web URL to the message |
|
| `url` | string | Web URL to the message |
|
||||||
| `updatedContent` | boolean | Whether content was successfully updated |
|
| `updatedContent` | boolean | Whether content was successfully updated |
|
||||||
|
| `files` | file[] | Files attached to the message |
|
||||||
|
|
||||||
### `microsoft_teams_read_channel`
|
### `microsoft_teams_read_channel`
|
||||||
|
|
||||||
@@ -132,6 +133,7 @@ Write or send a message to a Microsoft Teams channel
|
|||||||
| `createdTime` | string | Timestamp when message was created |
|
| `createdTime` | string | Timestamp when message was created |
|
||||||
| `url` | string | Web URL to the message |
|
| `url` | string | Web URL to the message |
|
||||||
| `updatedContent` | boolean | Whether content was successfully updated |
|
| `updatedContent` | boolean | Whether content was successfully updated |
|
||||||
|
| `files` | file[] | Files attached to the message |
|
||||||
|
|
||||||
### `microsoft_teams_update_chat_message`
|
### `microsoft_teams_update_chat_message`
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Extract text from PDF documents
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="mistral_parse_v2"
|
type="mistral_parse_v3"
|
||||||
color="#000000"
|
color="#000000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -35,13 +35,12 @@ Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF do
|
|||||||
|
|
||||||
### `mistral_parser`
|
### `mistral_parser`
|
||||||
|
|
||||||
Parse PDF documents using Mistral OCR API
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `filePath` | string | Yes | URL to a PDF document to be processed |
|
| `filePath` | string | No | URL to a PDF document to be processed |
|
||||||
|
| `file` | file | No | Document file to be processed |
|
||||||
| `fileUpload` | object | No | File upload data from file-upload component |
|
| `fileUpload` | object | No | File upload data from file-upload component |
|
||||||
| `resultType` | string | No | Type of parsed result \(markdown, text, or json\). Defaults to markdown. |
|
| `resultType` | string | No | Type of parsed result \(markdown, text, or json\). Defaults to markdown. |
|
||||||
| `includeImageBase64` | boolean | No | Include base64-encoded images in the response |
|
| `includeImageBase64` | boolean | No | Include base64-encoded images in the response |
|
||||||
@@ -55,27 +54,8 @@ Parse PDF documents using Mistral OCR API
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `pages` | array | Array of page objects from Mistral OCR |
|
| `pages` | array | Array of page objects from Mistral OCR |
|
||||||
| ↳ `index` | number | Page index \(zero-based\) |
|
| `model` | string | Mistral OCR model identifier |
|
||||||
| ↳ `markdown` | string | Extracted markdown content |
|
| `usage_info` | json | Usage statistics from the API |
|
||||||
| ↳ `images` | array | Images extracted from this page with bounding boxes |
|
| `document_annotation` | string | Structured annotation data |
|
||||||
| ↳ `id` | string | Image identifier \(e.g., img-0.jpeg\) |
|
|
||||||
| ↳ `top_left_x` | number | Top-left X coordinate in pixels |
|
|
||||||
| ↳ `top_left_y` | number | Top-left Y coordinate in pixels |
|
|
||||||
| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels |
|
|
||||||
| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels |
|
|
||||||
| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) |
|
|
||||||
| ↳ `dimensions` | object | Page dimensions |
|
|
||||||
| ↳ `dpi` | number | Dots per inch |
|
|
||||||
| ↳ `height` | number | Page height in pixels |
|
|
||||||
| ↳ `width` | number | Page width in pixels |
|
|
||||||
| ↳ `tables` | array | Extracted tables as HTML/markdown \(when table_format is set\). Referenced via placeholders like \[tbl-0.html\] |
|
|
||||||
| ↳ `hyperlinks` | array | Array of URL strings detected in the page \(e.g., \["https://...", "mailto:..."\]\) |
|
|
||||||
| ↳ `header` | string | Page header content \(when extract_header=true\) |
|
|
||||||
| ↳ `footer` | string | Page footer content \(when extract_footer=true\) |
|
|
||||||
| `model` | string | Mistral OCR model identifier \(e.g., mistral-ocr-latest\) |
|
|
||||||
| `usage_info` | object | Usage and processing statistics |
|
|
||||||
| ↳ `pages_processed` | number | Total number of pages processed |
|
|
||||||
| ↳ `doc_size_bytes` | number | Document file size in bytes |
|
|
||||||
| `document_annotation` | string | Structured annotation data as JSON string \(when applicable\) |
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
color="#181C1E"
|
color="#181C1E"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
|
The Notion tool integration enables your agents to read, create, and manage Notion pages and databases directly within your workflows. This allows you to automate the retrieval and updating of structured content, notes, documents, and more from your Notion workspace.
|
||||||
|
|
||||||
|
With the Notion tool, you can:
|
||||||
|
|
||||||
|
- **Read pages or databases**: Extract rich content or metadata from specified Notion pages or entire databases
|
||||||
|
- **Create new content**: Programmatically create new pages or databases for dynamic content generation
|
||||||
|
- **Append content**: Add new blocks or properties to existing pages and databases
|
||||||
|
- **Query databases**: Run advanced filters and searches on structured Notion data for custom workflows
|
||||||
|
- **Search your workspace**: Locate pages and databases across your Notion workspace automatically
|
||||||
|
|
||||||
|
This tool is ideal for scenarios where agents need to synchronize information, generate reports, or maintain structured notes within Notion. By bringing Notion's capabilities into automated workflows, you empower your agents to interface with knowledge, documentation, and project management data programmatically and seamlessly.
|
||||||
|
{/* MANUAL-CONTENT-END */}
|
||||||
|
|
||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|
||||||
Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace.
|
Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace.
|
||||||
@@ -98,6 +113,26 @@ Create a new page in Notion
|
|||||||
| `last_edited_time` | string | ISO 8601 last edit timestamp |
|
| `last_edited_time` | string | ISO 8601 last edit timestamp |
|
||||||
| `title` | string | Page title |
|
| `title` | string | Page title |
|
||||||
|
|
||||||
|
### `notion_update_page`
|
||||||
|
|
||||||
|
Update properties of a Notion page
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `pageId` | string | Yes | The UUID of the Notion page to update |
|
||||||
|
| `properties` | json | Yes | JSON object of properties to update |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `id` | string | Page UUID |
|
||||||
|
| `url` | string | Notion page URL |
|
||||||
|
| `last_edited_time` | string | ISO 8601 last edit timestamp |
|
||||||
|
| `title` | string | Page title |
|
||||||
|
|
||||||
### `notion_query_database`
|
### `notion_query_database`
|
||||||
|
|
||||||
Query and filter Notion database entries with advanced filtering
|
Query and filter Notion database entries with advanced filtering
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ Retrieve files from Pipedrive with optional filters
|
|||||||
| `person_id` | string | No | Filter files by person ID \(e.g., "456"\) |
|
| `person_id` | string | No | Filter files by person ID \(e.g., "456"\) |
|
||||||
| `org_id` | string | No | Filter files by organization ID \(e.g., "789"\) |
|
| `org_id` | string | No | Filter files by organization ID \(e.g., "789"\) |
|
||||||
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
|
||||||
|
| `downloadFiles` | boolean | No | Download file contents into file outputs |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ Retrieve files from Pipedrive with optional filters
|
|||||||
| ↳ `person_id` | number | Associated person ID |
|
| ↳ `person_id` | number | Associated person ID |
|
||||||
| ↳ `org_id` | number | Associated organization ID |
|
| ↳ `org_id` | number | Associated organization ID |
|
||||||
| ↳ `url` | string | File download URL |
|
| ↳ `url` | string | File download URL |
|
||||||
|
| `downloadedFiles` | file[] | Downloaded files from Pipedrive |
|
||||||
| `total_items` | number | Total number of files returned |
|
| `total_items` | number | Total number of files returned |
|
||||||
| `success` | boolean | Operation success status |
|
| `success` | boolean | Operation success status |
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ description: Extract text from documents using Pulse OCR
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="pulse"
|
type="pulse_v2"
|
||||||
color="#E0E0E0"
|
color="#E0E0E0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* MANUAL-CONTENT-START:intro */}
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
The [Pulse](https://www.pulseapi.com/) tool enables seamless extraction of text and structured content from a wide variety of documents—including PDFs, images, and Office files—using state-of-the-art OCR (Optical Character Recognition) powered by Pulse. Designed for automated agentic workflows, Pulse Parser makes it easy to unlock valuable information trapped in unstructured documents and integrate the extracted content directly into your workflow.
|
The [Pulse](https://www.runpulse.com) tool enables seamless extraction of text and structured content from a wide variety of documents—including PDFs, images, and Office files—using state-of-the-art OCR (Optical Character Recognition) powered by Pulse. Designed for automated agentic workflows, Pulse Parser makes it easy to unlock valuable information trapped in unstructured documents and integrate the extracted content directly into your workflow.
|
||||||
|
|
||||||
With Pulse, you can:
|
With Pulse, you can:
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ If you need accurate, scalable, and developer-friendly document parsing capabili
|
|||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|
||||||
Integrate Pulse into the workflow. Extract text from PDF documents, images, and Office files via URL or upload.
|
Integrate Pulse into the workflow. Extract text from PDF documents, images, and Office files via upload or file references.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -39,13 +39,12 @@ Integrate Pulse into the workflow. Extract text from PDF documents, images, and
|
|||||||
|
|
||||||
### `pulse_parser`
|
### `pulse_parser`
|
||||||
|
|
||||||
Parse documents (PDF, images, Office docs) using Pulse OCR API
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `filePath` | string | Yes | URL to a document to be processed |
|
| `filePath` | string | No | URL to a document to be processed |
|
||||||
|
| `file` | file | No | Document file to be processed |
|
||||||
| `fileUpload` | object | No | File upload data from file-upload component |
|
| `fileUpload` | object | No | File upload data from file-upload component |
|
||||||
| `pages` | string | No | Page range to process \(1-indexed, e.g., "1-2,5"\) |
|
| `pages` | string | No | Page range to process \(1-indexed, e.g., "1-2,5"\) |
|
||||||
| `extractFigure` | boolean | No | Enable figure extraction from the document |
|
| `extractFigure` | boolean | No | Enable figure extraction from the document |
|
||||||
@@ -57,16 +56,6 @@ Parse documents (PDF, images, Office docs) using Pulse OCR API
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `markdown` | string | Extracted content in markdown format |
|
|
||||||
| `page_count` | number | Number of pages in the document |
|
|
||||||
| `job_id` | string | Unique job identifier |
|
|
||||||
| `bounding_boxes` | json | Bounding box layout information |
|
|
||||||
| `extraction_url` | string | URL for extraction results \(for large documents\) |
|
|
||||||
| `html` | string | HTML content if requested |
|
|
||||||
| `structured_output` | json | Structured output if schema was provided |
|
|
||||||
| `chunks` | json | Chunked content if chunking was enabled |
|
|
||||||
| `figures` | json | Extracted figures if figure extraction was enabled |
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Extract text from PDF documents
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="reducto"
|
type="reducto_v2"
|
||||||
color="#5c0c5c"
|
color="#5c0c5c"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ Looking for reliable and scalable PDF parsing? Reducto is optimized for develope
|
|||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|
||||||
Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL.
|
Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF documents or file references.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -37,13 +37,12 @@ Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF do
|
|||||||
|
|
||||||
### `reducto_parser`
|
### `reducto_parser`
|
||||||
|
|
||||||
Parse PDF documents using Reducto OCR API
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `filePath` | string | Yes | URL to a PDF document to be processed |
|
| `filePath` | string | No | URL to a PDF document to be processed |
|
||||||
|
| `file` | file | No | Document file to be processed |
|
||||||
| `fileUpload` | object | No | File upload data from file-upload component |
|
| `fileUpload` | object | No | File upload data from file-upload component |
|
||||||
| `pages` | array | No | Specific pages to process \(1-indexed page numbers\) |
|
| `pages` | array | No | Specific pages to process \(1-indexed page numbers\) |
|
||||||
| `tableOutputFormat` | string | No | Table output format \(html or markdown\). Defaults to markdown. |
|
| `tableOutputFormat` | string | No | Table output format \(html or markdown\). Defaults to markdown. |
|
||||||
@@ -51,13 +50,6 @@ Parse PDF documents using Reducto OCR API
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `job_id` | string | Unique identifier for the processing job |
|
|
||||||
| `duration` | number | Processing time in seconds |
|
|
||||||
| `usage` | json | Resource consumption data |
|
|
||||||
| `result` | json | Parsed document content with chunks and blocks |
|
|
||||||
| `pdf_url` | string | Storage URL of converted PDF |
|
|
||||||
| `studio_link` | string | Link to Reducto studio interface |
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Retrieve an object from an AWS S3 bucket
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `url` | string | Pre-signed URL for downloading the S3 object |
|
| `url` | string | Pre-signed URL for downloading the S3 object |
|
||||||
|
| `file` | file | Downloaded file stored in execution files |
|
||||||
| `metadata` | object | File metadata including type, size, name, and last modified date |
|
| `metadata` | object | File metadata including type, size, name, and last modified date |
|
||||||
|
|
||||||
### `s3_list_objects`
|
### `s3_list_objects`
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Send an email using SendGrid API
|
|||||||
| `bcc` | string | No | BCC email address |
|
| `bcc` | string | No | BCC email address |
|
||||||
| `replyTo` | string | No | Reply-to email address |
|
| `replyTo` | string | No | Reply-to email address |
|
||||||
| `replyToName` | string | No | Reply-to name |
|
| `replyToName` | string | No | Reply-to name |
|
||||||
| `attachments` | file[] | No | Files to attach to the email as an array of attachment objects |
|
| `attachments` | file[] | No | Files to attach to the email \(UserFile objects\) |
|
||||||
| `templateId` | string | No | SendGrid template ID to use |
|
| `templateId` | string | No | SendGrid template ID to use |
|
||||||
| `dynamicTemplateData` | json | No | JSON object of dynamic template data |
|
| `dynamicTemplateData` | json | No | JSON object of dynamic template data |
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ Download a file from a remote SFTP server
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `success` | boolean | Whether the download was successful |
|
| `success` | boolean | Whether the download was successful |
|
||||||
|
| `file` | file | Downloaded file stored in execution files |
|
||||||
| `fileName` | string | Name of the downloaded file |
|
| `fileName` | string | Name of the downloaded file |
|
||||||
| `content` | string | File content \(text or base64 encoded\) |
|
| `content` | string | File content \(text or base64 encoded\) |
|
||||||
| `size` | number | File size in bytes |
|
| `size` | number | File size in bytes |
|
||||||
|
|||||||
@@ -13,16 +13,6 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
{/* MANUAL-CONTENT-START:intro */}
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
[Slack](https://www.slack.com/) is a business communication platform that offers teams a unified place for messaging, tools, and files.
|
[Slack](https://www.slack.com/) is a business communication platform that offers teams a unified place for messaging, tools, and files.
|
||||||
|
|
||||||
<iframe
|
|
||||||
width="100%"
|
|
||||||
height="400"
|
|
||||||
src="https://www.youtube.com/embed/J5jz3UaWmE8"
|
|
||||||
title="Slack Integration with Sim"
|
|
||||||
frameBorder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
With Slack, you can:
|
With Slack, you can:
|
||||||
|
|
||||||
- **Automate agent notifications**: Send real-time updates from your Sim agents to any Slack channel
|
- **Automate agent notifications**: Send real-time updates from your Sim agents to any Slack channel
|
||||||
@@ -154,6 +144,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
|
|||||||
| `ts` | string | Message timestamp |
|
| `ts` | string | Message timestamp |
|
||||||
| `channel` | string | Channel ID where message was sent |
|
| `channel` | string | Channel ID where message was sent |
|
||||||
| `fileCount` | number | Number of files uploaded \(when files are attached\) |
|
| `fileCount` | number | Number of files uploaded \(when files are attached\) |
|
||||||
|
| `files` | file[] | Files attached to the message |
|
||||||
|
|
||||||
### `slack_canvas`
|
### `slack_canvas`
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ Download a file from a remote SSH server
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `downloaded` | boolean | Whether the file was downloaded successfully |
|
| `downloaded` | boolean | Whether the file was downloaded successfully |
|
||||||
|
| `file` | file | Downloaded file stored in execution files |
|
||||||
| `fileContent` | string | File content \(base64 encoded for binary files\) |
|
| `fileContent` | string | File content \(base64 encoded for binary files\) |
|
||||||
| `fileName` | string | Name of the downloaded file |
|
| `fileName` | string | Name of the downloaded file |
|
||||||
| `remotePath` | string | Source path on the remote server |
|
| `remotePath` | string | Source path on the remote server |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Convert speech to text using AI
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="stt"
|
type="stt_v2"
|
||||||
color="#181C1E"
|
color="#181C1E"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -50,8 +50,6 @@ Transcribe audio and video files to text using leading AI providers. Supports mu
|
|||||||
|
|
||||||
### `stt_whisper`
|
### `stt_whisper`
|
||||||
|
|
||||||
Transcribe audio to text using OpenAI Whisper
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
@@ -71,22 +69,10 @@ Transcribe audio to text using OpenAI Whisper
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `transcript` | string | Full transcribed text |
|
|
||||||
| `segments` | array | Timestamped segments |
|
|
||||||
| ↳ `text` | string | Transcribed text for this segment |
|
|
||||||
| ↳ `start` | number | Start time in seconds |
|
|
||||||
| ↳ `end` | number | End time in seconds |
|
|
||||||
| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) |
|
|
||||||
| ↳ `confidence` | number | Confidence score \(0-1\) |
|
|
||||||
| `language` | string | Detected or specified language |
|
|
||||||
| `duration` | number | Audio duration in seconds |
|
|
||||||
|
|
||||||
### `stt_deepgram`
|
### `stt_deepgram`
|
||||||
|
|
||||||
Transcribe audio to text using Deepgram
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
@@ -103,23 +89,10 @@ Transcribe audio to text using Deepgram
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `transcript` | string | Full transcribed text |
|
|
||||||
| `segments` | array | Timestamped segments with speaker labels |
|
|
||||||
| ↳ `text` | string | Transcribed text for this segment |
|
|
||||||
| ↳ `start` | number | Start time in seconds |
|
|
||||||
| ↳ `end` | number | End time in seconds |
|
|
||||||
| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) |
|
|
||||||
| ↳ `confidence` | number | Confidence score \(0-1\) |
|
|
||||||
| `language` | string | Detected or specified language |
|
|
||||||
| `duration` | number | Audio duration in seconds |
|
|
||||||
| `confidence` | number | Overall confidence score |
|
|
||||||
|
|
||||||
### `stt_elevenlabs`
|
### `stt_elevenlabs`
|
||||||
|
|
||||||
Transcribe audio to text using ElevenLabs
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
@@ -135,18 +108,10 @@ Transcribe audio to text using ElevenLabs
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `transcript` | string | Full transcribed text |
|
|
||||||
| `segments` | array | Timestamped segments |
|
|
||||||
| `language` | string | Detected or specified language |
|
|
||||||
| `duration` | number | Audio duration in seconds |
|
|
||||||
| `confidence` | number | Overall confidence score |
|
|
||||||
|
|
||||||
### `stt_assemblyai`
|
### `stt_assemblyai`
|
||||||
|
|
||||||
Transcribe audio to text using AssemblyAI with advanced NLP features
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
@@ -167,35 +132,10 @@ Transcribe audio to text using AssemblyAI with advanced NLP features
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `transcript` | string | Full transcribed text |
|
|
||||||
| `segments` | array | Timestamped segments with speaker labels |
|
|
||||||
| ↳ `text` | string | Transcribed text for this segment |
|
|
||||||
| ↳ `start` | number | Start time in seconds |
|
|
||||||
| ↳ `end` | number | End time in seconds |
|
|
||||||
| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) |
|
|
||||||
| ↳ `confidence` | number | Confidence score \(0-1\) |
|
|
||||||
| `language` | string | Detected or specified language |
|
|
||||||
| `duration` | number | Audio duration in seconds |
|
|
||||||
| `confidence` | number | Overall confidence score |
|
|
||||||
| `sentiment` | array | Sentiment analysis results |
|
|
||||||
| ↳ `text` | string | Text that was analyzed |
|
|
||||||
| ↳ `sentiment` | string | Sentiment \(POSITIVE, NEGATIVE, NEUTRAL\) |
|
|
||||||
| ↳ `confidence` | number | Confidence score |
|
|
||||||
| ↳ `start` | number | Start time in milliseconds |
|
|
||||||
| ↳ `end` | number | End time in milliseconds |
|
|
||||||
| `entities` | array | Detected entities |
|
|
||||||
| ↳ `entity_type` | string | Entity type \(e.g., person_name, location, organization\) |
|
|
||||||
| ↳ `text` | string | Entity text |
|
|
||||||
| ↳ `start` | number | Start time in milliseconds |
|
|
||||||
| ↳ `end` | number | End time in milliseconds |
|
|
||||||
| `summary` | string | Auto-generated summary |
|
|
||||||
|
|
||||||
### `stt_gemini`
|
### `stt_gemini`
|
||||||
|
|
||||||
Transcribe audio to text using Google Gemini with multimodal capabilities
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
@@ -211,12 +151,6 @@ Transcribe audio to text using Google Gemini with multimodal capabilities
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `transcript` | string | Full transcribed text |
|
|
||||||
| `segments` | array | Timestamped segments |
|
|
||||||
| `language` | string | Detected or specified language |
|
|
||||||
| `duration` | number | Audio duration in seconds |
|
|
||||||
| `confidence` | number | Overall confidence score |
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `message` | string | Success or error message |
|
| `message` | string | Success or error message |
|
||||||
|
| `files` | file[] | Files attached to the message |
|
||||||
| `data` | object | Telegram message data including document |
|
| `data` | object | Telegram message data including document |
|
||||||
| ↳ `message_id` | number | Unique Telegram message identifier |
|
| ↳ `message_id` | number | Unique Telegram message identifier |
|
||||||
| ↳ `from` | object | Information about the sender |
|
| ↳ `from` | object | Information about the sender |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Extract text, tables, and forms from documents
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="textract"
|
type="textract_v2"
|
||||||
color="linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)"
|
color="linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -35,8 +35,6 @@ Integrate AWS Textract into your workflow to extract text, tables, forms, and ke
|
|||||||
|
|
||||||
### `textract_parser`
|
### `textract_parser`
|
||||||
|
|
||||||
Parse documents using AWS Textract OCR and document analysis
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
@@ -46,8 +44,8 @@ Parse documents using AWS Textract OCR and document analysis
|
|||||||
| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) |
|
| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) |
|
||||||
| `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. |
|
| `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. |
|
||||||
| `filePath` | string | No | URL to a document to be processed \(JPEG, PNG, or single-page PDF\). |
|
| `filePath` | string | No | URL to a document to be processed \(JPEG, PNG, or single-page PDF\). |
|
||||||
|
| `file` | file | No | Document file to be processed \(JPEG, PNG, or single-page PDF\). |
|
||||||
| `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). |
|
| `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). |
|
||||||
| `fileUpload` | object | No | File upload data from file-upload component |
|
|
||||||
| `featureTypes` | array | No | Feature types to detect: TABLES, FORMS, QUERIES, SIGNATURES, LAYOUT. If not specified, only text detection is performed. |
|
| `featureTypes` | array | No | Feature types to detect: TABLES, FORMS, QUERIES, SIGNATURES, LAYOUT. If not specified, only text detection is performed. |
|
||||||
| `items` | string | No | Feature type |
|
| `items` | string | No | Feature type |
|
||||||
| `queries` | array | No | Custom queries to extract specific information. Only used when featureTypes includes QUERIES. |
|
| `queries` | array | No | Custom queries to extract specific information. Only used when featureTypes includes QUERIES. |
|
||||||
@@ -58,39 +56,6 @@ Parse documents using AWS Textract OCR and document analysis
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `blocks` | array | Array of Block objects containing detected text, tables, forms, and other elements |
|
|
||||||
| ↳ `BlockType` | string | Type of block \(PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, etc.\) |
|
|
||||||
| ↳ `Id` | string | Unique identifier for the block |
|
|
||||||
| ↳ `Text` | string | The text content \(for LINE and WORD blocks\) |
|
|
||||||
| ↳ `TextType` | string | Type of text \(PRINTED or HANDWRITING\) |
|
|
||||||
| ↳ `Confidence` | number | Confidence score \(0-100\) |
|
|
||||||
| ↳ `Page` | number | Page number |
|
|
||||||
| ↳ `Geometry` | object | Location and bounding box information |
|
|
||||||
| ↳ `BoundingBox` | object | Height as ratio of document height |
|
|
||||||
| ↳ `Height` | number | Height as ratio of document height |
|
|
||||||
| ↳ `Left` | number | Left position as ratio of document width |
|
|
||||||
| ↳ `Top` | number | Top position as ratio of document height |
|
|
||||||
| ↳ `Width` | number | Width as ratio of document width |
|
|
||||||
| ↳ `Polygon` | array | Polygon coordinates |
|
|
||||||
| ↳ `X` | number | X coordinate |
|
|
||||||
| ↳ `Y` | number | Y coordinate |
|
|
||||||
| ↳ `Relationships` | array | Relationships to other blocks |
|
|
||||||
| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) |
|
|
||||||
| ↳ `Ids` | array | IDs of related blocks |
|
|
||||||
| ↳ `EntityTypes` | array | Entity types for KEY_VALUE_SET \(KEY or VALUE\) |
|
|
||||||
| ↳ `SelectionStatus` | string | For checkboxes: SELECTED or NOT_SELECTED |
|
|
||||||
| ↳ `RowIndex` | number | Row index for table cells |
|
|
||||||
| ↳ `ColumnIndex` | number | Column index for table cells |
|
|
||||||
| ↳ `RowSpan` | number | Row span for merged cells |
|
|
||||||
| ↳ `ColumnSpan` | number | Column span for merged cells |
|
|
||||||
| ↳ `Query` | object | Query information for QUERY blocks |
|
|
||||||
| ↳ `Text` | string | Query text |
|
|
||||||
| ↳ `Alias` | string | Query alias |
|
|
||||||
| ↳ `Pages` | array | Pages to search |
|
|
||||||
| `documentMetadata` | object | Metadata about the analyzed document |
|
|
||||||
| ↳ `pages` | number | Number of pages in the document |
|
|
||||||
| `modelVersion` | string | Version of the Textract model used for processing |
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ Retrieve call recording information and transcription (if enabled via TwiML).
|
|||||||
| `channels` | number | Number of channels \(1 for mono, 2 for dual\) |
|
| `channels` | number | Number of channels \(1 for mono, 2 for dual\) |
|
||||||
| `source` | string | How the recording was created |
|
| `source` | string | How the recording was created |
|
||||||
| `mediaUrl` | string | URL to download the recording media file |
|
| `mediaUrl` | string | URL to download the recording media file |
|
||||||
|
| `file` | file | Downloaded recording media file |
|
||||||
| `price` | string | Cost of the recording |
|
| `price` | string | Cost of the recording |
|
||||||
| `priceUnit` | string | Currency of the price |
|
| `priceUnit` | string | Currency of the price |
|
||||||
| `uri` | string | Relative URI of the recording resource |
|
| `uri` | string | Relative URI of the recording resource |
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ Download files uploaded in Typeform responses
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `fileUrl` | string | Direct download URL for the uploaded file |
|
| `fileUrl` | string | Direct download URL for the uploaded file |
|
||||||
|
| `file` | file | Downloaded file stored in execution files |
|
||||||
| `contentType` | string | MIME type of the uploaded file |
|
| `contentType` | string | MIME type of the uploaded file |
|
||||||
| `filename` | string | Original filename of the uploaded file |
|
| `filename` | string | Original filename of the uploaded file |
|
||||||
|
|
||||||
|
|||||||
@@ -57,14 +57,14 @@ Generate videos using Runway Gen-4 with world consistency and visual references
|
|||||||
| `duration` | number | No | Video duration in seconds \(5 or 10, default: 5\) |
|
| `duration` | number | No | Video duration in seconds \(5 or 10, default: 5\) |
|
||||||
| `aspectRatio` | string | No | Aspect ratio: 16:9 \(landscape\), 9:16 \(portrait\), or 1:1 \(square\) |
|
| `aspectRatio` | string | No | Aspect ratio: 16:9 \(landscape\), 9:16 \(portrait\), or 1:1 \(square\) |
|
||||||
| `resolution` | string | No | Video resolution \(720p output\). Note: Gen-4 Turbo outputs at 720p natively |
|
| `resolution` | string | No | Video resolution \(720p output\). Note: Gen-4 Turbo outputs at 720p natively |
|
||||||
| `visualReference` | json | Yes | Reference image REQUIRED for Gen-4 \(UserFile object\). Gen-4 only supports image-to-video, not text-only generation |
|
| `visualReference` | file | Yes | Reference image REQUIRED for Gen-4 \(UserFile object\). Gen-4 only supports image-to-video, not text-only generation |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `videoUrl` | string | Generated video URL |
|
| `videoUrl` | string | Generated video URL |
|
||||||
| `videoFile` | json | Video file object with metadata |
|
| `videoFile` | file | Video file object with metadata |
|
||||||
| `duration` | number | Video duration in seconds |
|
| `duration` | number | Video duration in seconds |
|
||||||
| `width` | number | Video width in pixels |
|
| `width` | number | Video width in pixels |
|
||||||
| `height` | number | Video height in pixels |
|
| `height` | number | Video height in pixels |
|
||||||
@@ -93,7 +93,7 @@ Generate videos using Google Veo 3/3.1 with native audio generation
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `videoUrl` | string | Generated video URL |
|
| `videoUrl` | string | Generated video URL |
|
||||||
| `videoFile` | json | Video file object with metadata |
|
| `videoFile` | file | Video file object with metadata |
|
||||||
| `duration` | number | Video duration in seconds |
|
| `duration` | number | Video duration in seconds |
|
||||||
| `width` | number | Video width in pixels |
|
| `width` | number | Video width in pixels |
|
||||||
| `height` | number | Video height in pixels |
|
| `height` | number | Video height in pixels |
|
||||||
@@ -123,7 +123,7 @@ Generate videos using Luma Dream Machine with advanced camera controls
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `videoUrl` | string | Generated video URL |
|
| `videoUrl` | string | Generated video URL |
|
||||||
| `videoFile` | json | Video file object with metadata |
|
| `videoFile` | file | Video file object with metadata |
|
||||||
| `duration` | number | Video duration in seconds |
|
| `duration` | number | Video duration in seconds |
|
||||||
| `width` | number | Video width in pixels |
|
| `width` | number | Video width in pixels |
|
||||||
| `height` | number | Video height in pixels |
|
| `height` | number | Video height in pixels |
|
||||||
@@ -151,7 +151,7 @@ Generate videos using MiniMax Hailuo through MiniMax Platform API with advanced
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `videoUrl` | string | Generated video URL |
|
| `videoUrl` | string | Generated video URL |
|
||||||
| `videoFile` | json | Video file object with metadata |
|
| `videoFile` | file | Video file object with metadata |
|
||||||
| `duration` | number | Video duration in seconds |
|
| `duration` | number | Video duration in seconds |
|
||||||
| `width` | number | Video width in pixels |
|
| `width` | number | Video width in pixels |
|
||||||
| `height` | number | Video height in pixels |
|
| `height` | number | Video height in pixels |
|
||||||
@@ -181,7 +181,7 @@ Generate videos using Fal.ai platform with access to multiple models including V
|
|||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `videoUrl` | string | Generated video URL |
|
| `videoUrl` | string | Generated video URL |
|
||||||
| `videoFile` | json | Video file object with metadata |
|
| `videoFile` | file | Video file object with metadata |
|
||||||
| `duration` | number | Video duration in seconds |
|
| `duration` | number | Video duration in seconds |
|
||||||
| `width` | number | Video width in pixels |
|
| `width` | number | Video width in pixels |
|
||||||
| `height` | number | Video height in pixels |
|
| `height` | number | Video height in pixels |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Analyze images with vision models
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="vision"
|
type="vision_v2"
|
||||||
color="#4D5FFF"
|
color="#4D5FFF"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -35,8 +35,6 @@ Integrate Vision into the workflow. Can analyze images with vision models.
|
|||||||
|
|
||||||
### `vision_tool`
|
### `vision_tool`
|
||||||
|
|
||||||
Process and analyze images using advanced vision models. Capable of understanding image content, extracting text, identifying objects, and providing detailed visual descriptions.
|
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
@@ -49,14 +47,6 @@ Process and analyze images using advanced vision models. Capable of understandin
|
|||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
This tool does not produce any outputs.
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `content` | string | The analyzed content and description of the image |
|
|
||||||
| `model` | string | The vision model that was used for analysis |
|
|
||||||
| `tokens` | number | Total tokens used for the analysis |
|
|
||||||
| `usage` | object | Detailed token usage breakdown |
|
|
||||||
| ↳ `input_tokens` | number | Tokens used for input processing |
|
|
||||||
| ↳ `output_tokens` | number | Tokens used for response generation |
|
|
||||||
| ↳ `total_tokens` | number | Total tokens consumed |
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ Get all recordings for a specific Zoom meeting
|
|||||||
| `meetingId` | string | Yes | The meeting ID or meeting UUID \(e.g., "1234567890" or "4444AAABBBccccc12345=="\) |
|
| `meetingId` | string | Yes | The meeting ID or meeting UUID \(e.g., "1234567890" or "4444AAABBBccccc12345=="\) |
|
||||||
| `includeFolderItems` | boolean | No | Include items within a folder |
|
| `includeFolderItems` | boolean | No | Include items within a folder |
|
||||||
| `ttl` | number | No | Time to live for download URLs in seconds \(max 604800\) |
|
| `ttl` | number | No | Time to live for download URLs in seconds \(max 604800\) |
|
||||||
|
| `downloadFiles` | boolean | No | Download recording files into file outputs |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
@@ -364,6 +365,7 @@ Get all recordings for a specific Zoom meeting
|
|||||||
| ↳ `download_url` | string | URL to download the recording |
|
| ↳ `download_url` | string | URL to download the recording |
|
||||||
| ↳ `status` | string | Recording status |
|
| ↳ `status` | string | Recording status |
|
||||||
| ↳ `recording_type` | string | Type of recording \(shared_screen, audio_only, etc.\) |
|
| ↳ `recording_type` | string | Type of recording \(shared_screen, audio_only, etc.\) |
|
||||||
|
| `files` | file[] | Downloaded recording files |
|
||||||
|
|
||||||
### `zoom_delete_recording`
|
### `zoom_delete_recording`
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Leer y analizar múltiples archivos
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="file"
|
type="file_v3"
|
||||||
color="#40916C"
|
color="#40916C"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Interactúa con transcripciones y grabaciones de reuniones de Firef
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="fireflies"
|
type="fireflies_v2"
|
||||||
color="#100730"
|
color="#100730"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Extraer texto de documentos PDF
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="mistral_parse"
|
type="mistral_parse_v3"
|
||||||
color="#000000"
|
color="#000000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Lire et analyser plusieurs fichiers
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="file"
|
type="file_v3"
|
||||||
color="#40916C"
|
color="#40916C"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: Interagissez avec les transcriptions et enregistrements de réunion
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="fireflies"
|
type="fireflies_v2"
|
||||||
color="#100730"
|
color="#100730"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Extraire du texte à partir de documents PDF
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="mistral_parse"
|
type="mistral_parse_v3"
|
||||||
color="#000000"
|
color="#000000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: 複数のファイルを読み込んで解析する
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="file"
|
type="file_v3"
|
||||||
color="#40916C"
|
color="#40916C"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Fireflies.aiの会議文字起こしと録画を操作
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="fireflies"
|
type="fireflies_v2"
|
||||||
color="#100730"
|
color="#100730"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: PDFドキュメントからテキストを抽出する
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="mistral_parse"
|
type="mistral_parse_v3"
|
||||||
color="#000000"
|
color="#000000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: 读取并解析多个文件
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="file"
|
type="file_v3"
|
||||||
color="#40916C"
|
color="#40916C"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: 与 Fireflies.ai 会议转录和录音进行交互
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="fireflies"
|
type="fireflies_v2"
|
||||||
color="#100730"
|
color="#100730"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: 从 PDF 文档中提取文本
|
|||||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="mistral_parse"
|
type="mistral_parse_v3"
|
||||||
color="#000000"
|
color="#000000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
BIN
apps/docs/public/static/quick-reference/lock-block.png
Normal file
BIN
apps/docs/public/static/quick-reference/lock-block.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useBrandConfig } from '@/lib/branding/branding'
|
|
||||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||||
|
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||||
|
|
||||||
export interface SupportFooterProps {
|
export interface SupportFooterProps {
|
||||||
/** Position style - 'fixed' for pages without AuthLayout, 'absolute' for pages with AuthLayout */
|
/** Position style - 'fixed' for pages without AuthLayout, 'absolute' for pages with AuthLayout */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||||
import SSOForm from '@/app/(auth)/sso/sso-form'
|
import SSOForm from '@/ee/sso/components/sso-form'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Database,
|
Database,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Workflow,
|
Timer,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
@@ -44,7 +44,7 @@ interface PricingTier {
|
|||||||
const FREE_PLAN_FEATURES: PricingFeature[] = [
|
const FREE_PLAN_FEATURES: PricingFeature[] = [
|
||||||
{ icon: DollarSign, text: '$20 usage limit' },
|
{ icon: DollarSign, text: '$20 usage limit' },
|
||||||
{ icon: HardDrive, text: '5GB file storage' },
|
{ icon: HardDrive, text: '5GB file storage' },
|
||||||
{ icon: Workflow, text: 'Public template access' },
|
{ icon: Timer, text: '5 min execution limit' },
|
||||||
{ icon: Database, text: 'Limited log retention' },
|
{ icon: Database, text: 'Limited log retention' },
|
||||||
{ icon: Code2, text: 'CLI/SDK Access' },
|
{ icon: Code2, text: 'CLI/SDK Access' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import Image from 'next/image'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { GithubIcon } from '@/components/icons'
|
import { GithubIcon } from '@/components/icons'
|
||||||
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 { useBrandConfig } from '@/ee/whitelabeling'
|
||||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||||
|
|
||||||
const logger = createLogger('nav')
|
const logger = createLogger('nav')
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ import {
|
|||||||
parseWorkflowSSEChunk,
|
parseWorkflowSSEChunk,
|
||||||
} from '@/lib/a2a/utils'
|
} from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
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 { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||||
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'
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
type PushNotificationSetParams,
|
type PushNotificationSetParams,
|
||||||
type TaskIdParams,
|
type TaskIdParams,
|
||||||
} from '@/app/api/a2a/serve/[agentId]/utils'
|
} from '@/app/api/a2a/serve/[agentId]/utils'
|
||||||
|
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||||
|
|
||||||
const logger = createLogger('A2AServeAPI')
|
const logger = createLogger('A2AServeAPI')
|
||||||
|
|
||||||
@@ -1119,7 +1119,7 @@ async function handlePushNotificationSet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlValidation = validateExternalUrl(
|
const urlValidation = await validateUrlWithDNS(
|
||||||
params.pushNotificationConfig.url,
|
params.pushNotificationConfig.url,
|
||||||
'Push notification URL'
|
'Push notification URL'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const UpdateCreatorProfileSchema = z.object({
|
|||||||
name: z.string().min(1, 'Name is required').max(100, 'Max 100 characters').optional(),
|
name: z.string().min(1, 'Name is required').max(100, 'Max 100 characters').optional(),
|
||||||
profileImageUrl: z.string().optional().or(z.literal('')),
|
profileImageUrl: z.string().optional().or(z.literal('')),
|
||||||
details: CreatorProfileDetailsSchema.optional(),
|
details: CreatorProfileDetailsSchema.optional(),
|
||||||
|
verified: z.boolean().optional(), // Verification status (super users only)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper to check if user has permission to manage profile
|
// Helper to check if user has permission to manage profile
|
||||||
@@ -97,11 +98,29 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
return NextResponse.json({ error: 'Profile not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Profile not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permissions
|
// Verification changes require super user permission
|
||||||
const canEdit = await hasPermission(session.user.id, existing[0])
|
if (data.verified !== undefined) {
|
||||||
if (!canEdit) {
|
const { verifyEffectiveSuperUser } = await import('@/lib/templates/permissions')
|
||||||
logger.warn(`[${requestId}] User denied permission to update profile: ${id}`)
|
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
|
||||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
if (!effectiveSuperUser) {
|
||||||
|
logger.warn(`[${requestId}] Non-super user attempted to change creator verification: ${id}`)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Only super users can change verification status' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-verified updates, check regular permissions
|
||||||
|
const hasNonVerifiedUpdates =
|
||||||
|
data.name !== undefined || data.profileImageUrl !== undefined || data.details !== undefined
|
||||||
|
|
||||||
|
if (hasNonVerifiedUpdates) {
|
||||||
|
const canEdit = await hasPermission(session.user.id, existing[0])
|
||||||
|
if (!canEdit) {
|
||||||
|
logger.warn(`[${requestId}] User denied permission to update profile: ${id}`)
|
||||||
|
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
@@ -111,6 +130,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
if (data.name !== undefined) updateData.name = data.name
|
if (data.name !== undefined) updateData.name = data.name
|
||||||
if (data.profileImageUrl !== undefined) updateData.profileImageUrl = data.profileImageUrl
|
if (data.profileImageUrl !== undefined) updateData.profileImageUrl = data.profileImageUrl
|
||||||
if (data.details !== undefined) updateData.details = data.details
|
if (data.details !== undefined) updateData.details = data.details
|
||||||
|
if (data.verified !== undefined) updateData.verified = data.verified
|
||||||
|
|
||||||
const updated = await db
|
const updated = await db
|
||||||
.update(templateCreators)
|
.update(templateCreators)
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import { db } from '@sim/db'
|
|
||||||
import { templateCreators } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { getSession } from '@/lib/auth'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
|
|
||||||
|
|
||||||
const logger = createLogger('CreatorVerificationAPI')
|
|
||||||
|
|
||||||
export const revalidate = 0
|
|
||||||
|
|
||||||
// POST /api/creators/[id]/verify - Verify a creator (super users only)
|
|
||||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { id } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const session = await getSession()
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
logger.warn(`[${requestId}] Unauthorized verification attempt for creator: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is a super user
|
|
||||||
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
|
|
||||||
if (!effectiveSuperUser) {
|
|
||||||
logger.warn(`[${requestId}] Non-super user attempted to verify creator: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Only super users can verify creators' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if creator exists
|
|
||||||
const existingCreator = await db
|
|
||||||
.select()
|
|
||||||
.from(templateCreators)
|
|
||||||
.where(eq(templateCreators.id, id))
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
if (existingCreator.length === 0) {
|
|
||||||
logger.warn(`[${requestId}] Creator not found for verification: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Creator not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update creator verified status to true
|
|
||||||
await db
|
|
||||||
.update(templateCreators)
|
|
||||||
.set({ verified: true, updatedAt: new Date() })
|
|
||||||
.where(eq(templateCreators.id, id))
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Creator verified: ${id} by super user: ${session.user.id}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
message: 'Creator verified successfully',
|
|
||||||
creatorId: id,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[${requestId}] Error verifying creator ${id}`, error)
|
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/creators/[id]/verify - Unverify a creator (super users only)
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { id } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const session = await getSession()
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
logger.warn(`[${requestId}] Unauthorized unverification attempt for creator: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is a super user
|
|
||||||
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
|
|
||||||
if (!effectiveSuperUser) {
|
|
||||||
logger.warn(`[${requestId}] Non-super user attempted to unverify creator: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Only super users can unverify creators' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if creator exists
|
|
||||||
const existingCreator = await db
|
|
||||||
.select()
|
|
||||||
.from(templateCreators)
|
|
||||||
.where(eq(templateCreators.id, id))
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
if (existingCreator.length === 0) {
|
|
||||||
logger.warn(`[${requestId}] Creator not found for unverification: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Creator not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update creator verified status to false
|
|
||||||
await db
|
|
||||||
.update(templateCreators)
|
|
||||||
.set({ verified: false, updatedAt: new Date() })
|
|
||||||
.where(eq(templateCreators.id, id))
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Creator unverified: ${id} by super user: ${session.user.id}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
message: 'Creator unverified successfully',
|
|
||||||
creatorId: id,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[${requestId}] Error unverifying creator ${id}`, error)
|
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import { db } from '@sim/db'
|
import { asyncJobs, db } from '@sim/db'
|
||||||
import { workflowExecutionLogs } from '@sim/db/schema'
|
import { workflowExecutionLogs } from '@sim/db/schema'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, lt, sql } from 'drizzle-orm'
|
import { and, eq, inArray, lt, sql } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { verifyCronAuth } from '@/lib/auth/internal'
|
import { verifyCronAuth } from '@/lib/auth/internal'
|
||||||
|
import { JOB_RETENTION_HOURS, JOB_STATUS } from '@/lib/core/async-jobs'
|
||||||
|
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||||
|
|
||||||
const logger = createLogger('CleanupStaleExecutions')
|
const logger = createLogger('CleanupStaleExecutions')
|
||||||
|
|
||||||
const STALE_THRESHOLD_MINUTES = 30
|
const STALE_THRESHOLD_MS = getMaxExecutionTimeout() + 5 * 60 * 1000
|
||||||
|
const STALE_THRESHOLD_MINUTES = Math.ceil(STALE_THRESHOLD_MS / 60000)
|
||||||
|
const MAX_INT32 = 2_147_483_647
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -45,13 +49,14 @@ export async function GET(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const staleDurationMs = Date.now() - new Date(execution.startedAt).getTime()
|
const staleDurationMs = Date.now() - new Date(execution.startedAt).getTime()
|
||||||
const staleDurationMinutes = Math.round(staleDurationMs / 60000)
|
const staleDurationMinutes = Math.round(staleDurationMs / 60000)
|
||||||
|
const totalDurationMs = Math.min(staleDurationMs, MAX_INT32)
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(workflowExecutionLogs)
|
.update(workflowExecutionLogs)
|
||||||
.set({
|
.set({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
endedAt: new Date(),
|
endedAt: new Date(),
|
||||||
totalDurationMs: staleDurationMs,
|
totalDurationMs,
|
||||||
executionData: sql`jsonb_set(
|
executionData: sql`jsonb_set(
|
||||||
COALESCE(execution_data, '{}'::jsonb),
|
COALESCE(execution_data, '{}'::jsonb),
|
||||||
ARRAY['error'],
|
ARRAY['error'],
|
||||||
@@ -76,12 +81,102 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
logger.info(`Stale execution cleanup completed. Cleaned: ${cleaned}, Failed: ${failed}`)
|
logger.info(`Stale execution cleanup completed. Cleaned: ${cleaned}, Failed: ${failed}`)
|
||||||
|
|
||||||
|
// Clean up stale async jobs (stuck in processing)
|
||||||
|
let asyncJobsMarkedFailed = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const staleAsyncJobs = await db
|
||||||
|
.update(asyncJobs)
|
||||||
|
.set({
|
||||||
|
status: JOB_STATUS.FAILED,
|
||||||
|
completedAt: new Date(),
|
||||||
|
error: `Job terminated: stuck in processing for more than ${STALE_THRESHOLD_MINUTES} minutes`,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(eq(asyncJobs.status, JOB_STATUS.PROCESSING), lt(asyncJobs.startedAt, staleThreshold))
|
||||||
|
)
|
||||||
|
.returning({ id: asyncJobs.id })
|
||||||
|
|
||||||
|
asyncJobsMarkedFailed = staleAsyncJobs.length
|
||||||
|
if (asyncJobsMarkedFailed > 0) {
|
||||||
|
logger.info(`Marked ${asyncJobsMarkedFailed} stale async jobs as failed`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to clean up stale async jobs:', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up stale pending jobs (never started, e.g., due to server crash before startJob())
|
||||||
|
let stalePendingJobsMarkedFailed = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stalePendingJobs = await db
|
||||||
|
.update(asyncJobs)
|
||||||
|
.set({
|
||||||
|
status: JOB_STATUS.FAILED,
|
||||||
|
completedAt: new Date(),
|
||||||
|
error: `Job terminated: stuck in pending state for more than ${STALE_THRESHOLD_MINUTES} minutes (never started)`,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(eq(asyncJobs.status, JOB_STATUS.PENDING), lt(asyncJobs.createdAt, staleThreshold))
|
||||||
|
)
|
||||||
|
.returning({ id: asyncJobs.id })
|
||||||
|
|
||||||
|
stalePendingJobsMarkedFailed = stalePendingJobs.length
|
||||||
|
if (stalePendingJobsMarkedFailed > 0) {
|
||||||
|
logger.info(`Marked ${stalePendingJobsMarkedFailed} stale pending jobs as failed`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to clean up stale pending jobs:', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete completed/failed jobs older than retention period
|
||||||
|
const retentionThreshold = new Date(Date.now() - JOB_RETENTION_HOURS * 60 * 60 * 1000)
|
||||||
|
let asyncJobsDeleted = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deletedJobs = await db
|
||||||
|
.delete(asyncJobs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(asyncJobs.status, [JOB_STATUS.COMPLETED, JOB_STATUS.FAILED]),
|
||||||
|
lt(asyncJobs.completedAt, retentionThreshold)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning({ id: asyncJobs.id })
|
||||||
|
|
||||||
|
asyncJobsDeleted = deletedJobs.length
|
||||||
|
if (asyncJobsDeleted > 0) {
|
||||||
|
logger.info(
|
||||||
|
`Deleted ${asyncJobsDeleted} old async jobs (retention: ${JOB_RETENTION_HOURS}h)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to delete old async jobs:', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
found: staleExecutions.length,
|
executions: {
|
||||||
cleaned,
|
found: staleExecutions.length,
|
||||||
failed,
|
cleaned,
|
||||||
thresholdMinutes: STALE_THRESHOLD_MINUTES,
|
failed,
|
||||||
|
thresholdMinutes: STALE_THRESHOLD_MINUTES,
|
||||||
|
},
|
||||||
|
asyncJobs: {
|
||||||
|
staleProcessingMarkedFailed: asyncJobsMarkedFailed,
|
||||||
|
stalePendingMarkedFailed: stalePendingJobsMarkedFailed,
|
||||||
|
oldDeleted: asyncJobsDeleted,
|
||||||
|
staleThresholdMinutes: STALE_THRESHOLD_MINUTES,
|
||||||
|
retentionHours: JOB_RETENTION_HOURS,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in stale execution cleanup job:', error)
|
logger.error('Error in stale execution cleanup job:', error)
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { createLogger } from '@sim/logger'
|
|||||||
import binaryExtensionsList from 'binary-extensions'
|
import binaryExtensionsList from 'binary-extensions'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { secureFetchWithPinnedIP, validateUrlWithDNS } from '@/lib/core/security/input-validation'
|
import {
|
||||||
|
secureFetchWithPinnedIP,
|
||||||
|
validateUrlWithDNS,
|
||||||
|
} from '@/lib/core/security/input-validation.server'
|
||||||
|
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||||
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
|
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
|
||||||
import { isUsingCloudStorage, type StorageContext, StorageService } from '@/lib/uploads'
|
import { isUsingCloudStorage, type StorageContext, StorageService } from '@/lib/uploads'
|
||||||
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
|
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
|
||||||
@@ -19,6 +23,7 @@ import {
|
|||||||
getMimeTypeFromExtension,
|
getMimeTypeFromExtension,
|
||||||
getViewerUrl,
|
getViewerUrl,
|
||||||
inferContextFromKey,
|
inferContextFromKey,
|
||||||
|
isInternalFileUrl,
|
||||||
} from '@/lib/uploads/utils/file-utils'
|
} from '@/lib/uploads/utils/file-utils'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
import { verifyFileAccess } from '@/app/api/files/authorization'
|
import { verifyFileAccess } from '@/app/api/files/authorization'
|
||||||
@@ -215,7 +220,7 @@ async function parseFileSingle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filePath.includes('/api/files/serve/')) {
|
if (isInternalFileUrl(filePath)) {
|
||||||
return handleCloudFile(filePath, fileType, undefined, userId, executionContext)
|
return handleCloudFile(filePath, fileType, undefined, userId, executionContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +251,7 @@ function validateFilePath(filePath: string): { isValid: boolean; error?: string
|
|||||||
return { isValid: false, error: 'Invalid path: tilde character not allowed' }
|
return { isValid: false, error: 'Invalid path: tilde character not allowed' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filePath.startsWith('/') && !filePath.startsWith('/api/files/serve/')) {
|
if (filePath.startsWith('/') && !isInternalFileUrl(filePath)) {
|
||||||
return { isValid: false, error: 'Path outside allowed directory' }
|
return { isValid: false, error: 'Path outside allowed directory' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +425,7 @@ async function handleExternalUrl(
|
|||||||
|
|
||||||
return parseResult
|
return parseResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error handling external URL ${url}:`, error)
|
logger.error(`Error handling external URL ${sanitizeUrlForLog(url)}:`, error)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Error fetching URL: ${(error as Error).message}`,
|
error: `Error fetching URL: ${(error as Error).message}`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { runs } from '@trigger.dev/sdk'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { getJobQueue, JOB_STATUS } from '@/lib/core/async-jobs'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { createErrorResponse } from '@/app/api/workflows/utils'
|
import { createErrorResponse } from '@/app/api/workflows/utils'
|
||||||
|
|
||||||
@@ -15,8 +15,6 @@ export async function GET(
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`[${requestId}] Getting status for task: ${taskId}`)
|
|
||||||
|
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized task status request`)
|
logger.warn(`[${requestId}] Unauthorized task status request`)
|
||||||
@@ -25,76 +23,60 @@ export async function GET(
|
|||||||
|
|
||||||
const authenticatedUserId = authResult.userId
|
const authenticatedUserId = authResult.userId
|
||||||
|
|
||||||
const run = await runs.retrieve(taskId)
|
const jobQueue = await getJobQueue()
|
||||||
|
const job = await jobQueue.getJob(taskId)
|
||||||
|
|
||||||
logger.debug(`[${requestId}] Task ${taskId} status: ${run.status}`)
|
if (!job) {
|
||||||
|
return createErrorResponse('Task not found', 404)
|
||||||
const payload = run.payload as any
|
|
||||||
if (payload?.workflowId) {
|
|
||||||
const { verifyWorkflowAccess } = await import('@/socket/middleware/permissions')
|
|
||||||
const accessCheck = await verifyWorkflowAccess(authenticatedUserId, payload.workflowId)
|
|
||||||
if (!accessCheck.hasAccess) {
|
|
||||||
logger.warn(`[${requestId}] User ${authenticatedUserId} denied access to task ${taskId}`, {
|
|
||||||
workflowId: payload.workflowId,
|
|
||||||
})
|
|
||||||
return createErrorResponse('Access denied', 403)
|
|
||||||
}
|
|
||||||
logger.debug(`[${requestId}] User ${authenticatedUserId} has access to task ${taskId}`)
|
|
||||||
} else {
|
|
||||||
if (payload?.userId && payload.userId !== authenticatedUserId) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] User ${authenticatedUserId} attempted to access task ${taskId} owned by ${payload.userId}`
|
|
||||||
)
|
|
||||||
return createErrorResponse('Access denied', 403)
|
|
||||||
}
|
|
||||||
if (!payload?.userId) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Task ${taskId} has no ownership information in payload. Denying access for security.`
|
|
||||||
)
|
|
||||||
return createErrorResponse('Access denied', 403)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusMap = {
|
if (job.metadata?.workflowId) {
|
||||||
QUEUED: 'queued',
|
const { verifyWorkflowAccess } = await import('@/socket/middleware/permissions')
|
||||||
WAITING_FOR_DEPLOY: 'queued',
|
const accessCheck = await verifyWorkflowAccess(
|
||||||
EXECUTING: 'processing',
|
authenticatedUserId,
|
||||||
RESCHEDULED: 'processing',
|
job.metadata.workflowId as string
|
||||||
FROZEN: 'processing',
|
)
|
||||||
COMPLETED: 'completed',
|
if (!accessCheck.hasAccess) {
|
||||||
CANCELED: 'cancelled',
|
logger.warn(`[${requestId}] Access denied to workflow ${job.metadata.workflowId}`)
|
||||||
FAILED: 'failed',
|
return createErrorResponse('Access denied', 403)
|
||||||
CRASHED: 'failed',
|
}
|
||||||
INTERRUPTED: 'failed',
|
} else if (job.metadata?.userId && job.metadata.userId !== authenticatedUserId) {
|
||||||
SYSTEM_FAILURE: 'failed',
|
logger.warn(`[${requestId}] Access denied to user ${job.metadata.userId}`)
|
||||||
EXPIRED: 'failed',
|
return createErrorResponse('Access denied', 403)
|
||||||
} as const
|
} else if (!job.metadata?.userId && !job.metadata?.workflowId) {
|
||||||
|
logger.warn(`[${requestId}] Access denied to job ${taskId}`)
|
||||||
|
return createErrorResponse('Access denied', 403)
|
||||||
|
}
|
||||||
|
|
||||||
const mappedStatus = statusMap[run.status as keyof typeof statusMap] || 'unknown'
|
const mappedStatus = job.status === JOB_STATUS.PENDING ? 'queued' : job.status
|
||||||
|
|
||||||
const response: any = {
|
const response: any = {
|
||||||
success: true,
|
success: true,
|
||||||
taskId,
|
taskId,
|
||||||
status: mappedStatus,
|
status: mappedStatus,
|
||||||
metadata: {
|
metadata: {
|
||||||
startedAt: run.startedAt,
|
startedAt: job.startedAt,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedStatus === 'completed') {
|
if (job.status === JOB_STATUS.COMPLETED) {
|
||||||
response.output = run.output // This contains the workflow execution results
|
response.output = job.output
|
||||||
response.metadata.completedAt = run.finishedAt
|
response.metadata.completedAt = job.completedAt
|
||||||
response.metadata.duration = run.durationMs
|
if (job.startedAt && job.completedAt) {
|
||||||
|
response.metadata.duration = job.completedAt.getTime() - job.startedAt.getTime()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedStatus === 'failed') {
|
if (job.status === JOB_STATUS.FAILED) {
|
||||||
response.error = run.error
|
response.error = job.error
|
||||||
response.metadata.completedAt = run.finishedAt
|
response.metadata.completedAt = job.completedAt
|
||||||
response.metadata.duration = run.durationMs
|
if (job.startedAt && job.completedAt) {
|
||||||
|
response.metadata.duration = job.completedAt.getTime() - job.startedAt.getTime()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedStatus === 'processing' || mappedStatus === 'queued') {
|
if (job.status === JOB_STATUS.PROCESSING || job.status === JOB_STATUS.PENDING) {
|
||||||
response.estimatedDuration = 180000 // 3 minutes max from our config
|
response.estimatedDuration = 180000
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(response)
|
return NextResponse.json(response)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateInternalToken } from '@/lib/auth/internal'
|
import { generateInternalToken } from '@/lib/auth/internal'
|
||||||
|
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
|
|
||||||
const logger = createLogger('WorkflowMcpServeAPI')
|
const logger = createLogger('WorkflowMcpServeAPI')
|
||||||
@@ -264,7 +265,7 @@ async function handleToolsCall(
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }),
|
body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }),
|
||||||
signal: AbortSignal.timeout(600000), // 10 minute timeout
|
signal: AbortSignal.timeout(getMaxExecutionTimeout()),
|
||||||
})
|
})
|
||||||
|
|
||||||
const executeResult = await response.json()
|
const executeResult = await response.json()
|
||||||
@@ -284,7 +285,7 @@ async function handleToolsCall(
|
|||||||
content: [
|
content: [
|
||||||
{ type: 'text', text: JSON.stringify(executeResult.output || executeResult, null, 2) },
|
{ type: 'text', text: JSON.stringify(executeResult.output || executeResult, null, 2) },
|
||||||
],
|
],
|
||||||
isError: !executeResult.success,
|
isError: executeResult.success === false,
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(createResponse(id, result))
|
return NextResponse.json(createResponse(id, result))
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
|
||||||
|
import { getExecutionTimeout } from '@/lib/core/execution-limits'
|
||||||
|
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { mcpService } from '@/lib/mcp/service'
|
import { mcpService } from '@/lib/mcp/service'
|
||||||
import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types'
|
import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types'
|
||||||
@@ -7,7 +10,6 @@ import {
|
|||||||
categorizeError,
|
categorizeError,
|
||||||
createMcpErrorResponse,
|
createMcpErrorResponse,
|
||||||
createMcpSuccessResponse,
|
createMcpSuccessResponse,
|
||||||
MCP_CONSTANTS,
|
|
||||||
validateStringParam,
|
validateStringParam,
|
||||||
} from '@/lib/mcp/utils'
|
} from '@/lib/mcp/utils'
|
||||||
|
|
||||||
@@ -171,13 +173,16 @@ export const POST = withMcpAuth('read')(
|
|||||||
arguments: args,
|
arguments: args,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userSubscription = await getHighestPrioritySubscription(userId)
|
||||||
|
const executionTimeout = getExecutionTimeout(
|
||||||
|
userSubscription?.plan as SubscriptionPlan | undefined,
|
||||||
|
'sync'
|
||||||
|
)
|
||||||
|
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
mcpService.executeTool(userId, serverId, toolCall, workspaceId),
|
mcpService.executeTool(userId, serverId, toolCall, workspaceId),
|
||||||
new Promise<never>((_, reject) =>
|
new Promise<never>((_, reject) =>
|
||||||
setTimeout(
|
setTimeout(() => reject(new Error('Tool execution timeout')), executionTimeout)
|
||||||
() => reject(new Error('Tool execution timeout')),
|
|
||||||
MCP_CONSTANTS.EXECUTION_TIMEOUT
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { z } from 'zod'
|
|||||||
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
|
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { hasAccessControlAccess } from '@/lib/billing'
|
import { hasAccessControlAccess } from '@/lib/billing'
|
||||||
|
import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage'
|
||||||
import { requireStripeClient } from '@/lib/billing/stripe-client'
|
import { requireStripeClient } from '@/lib/billing/stripe-client'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||||
@@ -501,6 +502,18 @@ export async function PUT(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status === 'accepted') {
|
||||||
|
try {
|
||||||
|
await syncUsageLimitsFromSubscription(session.user.id)
|
||||||
|
} catch (syncError) {
|
||||||
|
logger.error('Failed to sync usage limits after joining org', {
|
||||||
|
userId: session.user.id,
|
||||||
|
organizationId,
|
||||||
|
error: syncError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`Organization invitation ${status}`, {
|
logger.info(`Organization invitation ${status}`, {
|
||||||
organizationId,
|
organizationId,
|
||||||
invitationId,
|
invitationId,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
|
|||||||
import {
|
import {
|
||||||
InvitationsNotAllowedError,
|
InvitationsNotAllowedError,
|
||||||
validateInvitationsAllowed,
|
validateInvitationsAllowed,
|
||||||
} from '@/executor/utils/permission-check'
|
} from '@/ee/access-control/utils/permission-check'
|
||||||
|
|
||||||
const logger = createLogger('OrganizationInvitations')
|
const logger = createLogger('OrganizationInvitations')
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { db, workflowDeploymentVersion, workflowSchedule } from '@sim/db'
|
import { db, workflowDeploymentVersion, workflowSchedule } from '@sim/db'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { tasks } from '@trigger.dev/sdk'
|
|
||||||
import { and, eq, isNull, lt, lte, not, or, sql } from 'drizzle-orm'
|
import { and, eq, isNull, lt, lte, not, or, sql } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { verifyCronAuth } from '@/lib/auth/internal'
|
import { verifyCronAuth } from '@/lib/auth/internal'
|
||||||
import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags'
|
import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { executeScheduleJob } from '@/background/schedule-execution'
|
import { executeScheduleJob } from '@/background/schedule-execution'
|
||||||
|
|
||||||
@@ -55,72 +54,67 @@ export async function GET(request: NextRequest) {
|
|||||||
logger.debug(`[${requestId}] Successfully queried schedules: ${dueSchedules.length} found`)
|
logger.debug(`[${requestId}] Successfully queried schedules: ${dueSchedules.length} found`)
|
||||||
logger.info(`[${requestId}] Processing ${dueSchedules.length} due scheduled workflows`)
|
logger.info(`[${requestId}] Processing ${dueSchedules.length} due scheduled workflows`)
|
||||||
|
|
||||||
if (isTriggerDevEnabled) {
|
const jobQueue = await getJobQueue()
|
||||||
const triggerPromises = dueSchedules.map(async (schedule) => {
|
|
||||||
const queueTime = schedule.lastQueuedAt ?? queuedAt
|
|
||||||
|
|
||||||
try {
|
const queuePromises = dueSchedules.map(async (schedule) => {
|
||||||
const payload = {
|
const queueTime = schedule.lastQueuedAt ?? queuedAt
|
||||||
scheduleId: schedule.id,
|
|
||||||
workflowId: schedule.workflowId,
|
|
||||||
blockId: schedule.blockId || undefined,
|
|
||||||
cronExpression: schedule.cronExpression || undefined,
|
|
||||||
lastRanAt: schedule.lastRanAt?.toISOString(),
|
|
||||||
failedCount: schedule.failedCount || 0,
|
|
||||||
now: queueTime.toISOString(),
|
|
||||||
scheduledFor: schedule.nextRunAt?.toISOString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const handle = await tasks.trigger('schedule-execution', payload)
|
const payload = {
|
||||||
logger.info(
|
scheduleId: schedule.id,
|
||||||
`[${requestId}] Queued schedule execution task ${handle.id} for workflow ${schedule.workflowId}`
|
workflowId: schedule.workflowId,
|
||||||
)
|
blockId: schedule.blockId || undefined,
|
||||||
return handle
|
cronExpression: schedule.cronExpression || undefined,
|
||||||
} catch (error) {
|
lastRanAt: schedule.lastRanAt?.toISOString(),
|
||||||
logger.error(
|
failedCount: schedule.failedCount || 0,
|
||||||
`[${requestId}] Failed to trigger schedule execution for workflow ${schedule.workflowId}`,
|
now: queueTime.toISOString(),
|
||||||
error
|
scheduledFor: schedule.nextRunAt?.toISOString(),
|
||||||
)
|
}
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.allSettled(triggerPromises)
|
try {
|
||||||
|
const jobId = await jobQueue.enqueue('schedule-execution', payload, {
|
||||||
logger.info(`[${requestId}] Queued ${dueSchedules.length} schedule executions to Trigger.dev`)
|
metadata: { workflowId: schedule.workflowId },
|
||||||
} else {
|
|
||||||
const directExecutionPromises = dueSchedules.map(async (schedule) => {
|
|
||||||
const queueTime = schedule.lastQueuedAt ?? queuedAt
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
scheduleId: schedule.id,
|
|
||||||
workflowId: schedule.workflowId,
|
|
||||||
blockId: schedule.blockId || undefined,
|
|
||||||
cronExpression: schedule.cronExpression || undefined,
|
|
||||||
lastRanAt: schedule.lastRanAt?.toISOString(),
|
|
||||||
failedCount: schedule.failedCount || 0,
|
|
||||||
now: queueTime.toISOString(),
|
|
||||||
scheduledFor: schedule.nextRunAt?.toISOString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
void executeScheduleJob(payload).catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
`[${requestId}] Direct schedule execution failed for workflow ${schedule.workflowId}`,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${requestId}] Queued direct schedule execution for workflow ${schedule.workflowId} (Trigger.dev disabled)`
|
`[${requestId}] Queued schedule execution task ${jobId} for workflow ${schedule.workflowId}`
|
||||||
)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.allSettled(directExecutionPromises)
|
if (shouldExecuteInline()) {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await jobQueue.startJob(jobId)
|
||||||
|
const output = await executeScheduleJob(payload)
|
||||||
|
await jobQueue.completeJob(jobId, output)
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
logger.error(
|
||||||
|
`[${requestId}] Schedule execution failed for workflow ${schedule.workflowId}`,
|
||||||
|
{ jobId, error: errorMessage }
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
await jobQueue.markJobFailed(jobId, errorMessage)
|
||||||
|
} catch (markFailedError) {
|
||||||
|
logger.error(`[${requestId}] Failed to mark job as failed`, {
|
||||||
|
jobId,
|
||||||
|
error:
|
||||||
|
markFailedError instanceof Error
|
||||||
|
? markFailedError.message
|
||||||
|
: String(markFailedError),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[${requestId}] Failed to queue schedule execution for workflow ${schedule.workflowId}`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
logger.info(
|
await Promise.allSettled(queuePromises)
|
||||||
`[${requestId}] Queued ${dueSchedules.length} direct schedule executions (Trigger.dev disabled)`
|
|
||||||
)
|
logger.info(`[${requestId}] Queued ${dueSchedules.length} schedule executions`)
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: 'Scheduled workflow executions processed',
|
message: 'Scheduled workflow executions processed',
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import { db } from '@sim/db'
|
|
||||||
import { templates } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { getSession } from '@/lib/auth'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
|
|
||||||
|
|
||||||
const logger = createLogger('TemplateApprovalAPI')
|
|
||||||
|
|
||||||
export const revalidate = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/templates/[id]/approve - Approve a template (super users only)
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { id } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const session = await getSession()
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
logger.warn(`[${requestId}] Unauthorized template approval attempt for ID: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
|
|
||||||
if (!effectiveSuperUser) {
|
|
||||||
logger.warn(`[${requestId}] Non-super user attempted to approve template: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Only super users can approve templates' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
|
|
||||||
if (existingTemplate.length === 0) {
|
|
||||||
logger.warn(`[${requestId}] Template not found for approval: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(templates)
|
|
||||||
.set({ status: 'approved', updatedAt: new Date() })
|
|
||||||
.where(eq(templates.id, id))
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Template approved: ${id} by super user: ${session.user.id}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
message: 'Template approved successfully',
|
|
||||||
templateId: id,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[${requestId}] Error approving template ${id}`, error)
|
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /api/templates/[id]/approve - Unapprove a template (super users only)
|
|
||||||
*/
|
|
||||||
export async function DELETE(
|
|
||||||
_request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { id } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const session = await getSession()
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
logger.warn(`[${requestId}] Unauthorized template rejection attempt for ID: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
|
|
||||||
if (!effectiveSuperUser) {
|
|
||||||
logger.warn(`[${requestId}] Non-super user attempted to reject template: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Only super users can reject templates' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
|
|
||||||
if (existingTemplate.length === 0) {
|
|
||||||
logger.warn(`[${requestId}] Template not found for rejection: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(templates)
|
|
||||||
.set({ status: 'rejected', updatedAt: new Date() })
|
|
||||||
.where(eq(templates.id, id))
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Template rejected: ${id} by super user: ${session.user.id}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
message: 'Template rejected successfully',
|
|
||||||
templateId: id,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[${requestId}] Error rejecting template ${id}`, error)
|
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { db } from '@sim/db'
|
|
||||||
import { templates } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { getSession } from '@/lib/auth'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
|
|
||||||
|
|
||||||
const logger = createLogger('TemplateRejectionAPI')
|
|
||||||
|
|
||||||
export const revalidate = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/templates/[id]/reject - Reject a template (super users only)
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
const { id } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const session = await getSession()
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
logger.warn(`[${requestId}] Unauthorized template rejection attempt for ID: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
|
|
||||||
if (!effectiveSuperUser) {
|
|
||||||
logger.warn(`[${requestId}] Non-super user attempted to reject template: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Only super users can reject templates' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
|
|
||||||
if (existingTemplate.length === 0) {
|
|
||||||
logger.warn(`[${requestId}] Template not found for rejection: ${id}`)
|
|
||||||
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(templates)
|
|
||||||
.set({ status: 'rejected', updatedAt: new Date() })
|
|
||||||
.where(eq(templates.id, id))
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Template rejected: ${id} by super user: ${session.user.id}`)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
message: 'Template rejected successfully',
|
|
||||||
templateId: id,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[${requestId}] Error rejecting template ${id}`, error)
|
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -106,6 +106,7 @@ const updateTemplateSchema = z.object({
|
|||||||
creatorId: z.string().optional(), // Creator profile ID
|
creatorId: z.string().optional(), // Creator profile ID
|
||||||
tags: z.array(z.string()).max(10, 'Maximum 10 tags allowed').optional(),
|
tags: z.array(z.string()).max(10, 'Maximum 10 tags allowed').optional(),
|
||||||
updateState: z.boolean().optional(), // Explicitly request state update from current workflow
|
updateState: z.boolean().optional(), // Explicitly request state update from current workflow
|
||||||
|
status: z.enum(['approved', 'rejected', 'pending']).optional(), // Status change (super users only)
|
||||||
})
|
})
|
||||||
|
|
||||||
// PUT /api/templates/[id] - Update a template
|
// PUT /api/templates/[id] - Update a template
|
||||||
@@ -131,7 +132,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, details, creatorId, tags, updateState } = validationResult.data
|
const { name, details, creatorId, tags, updateState, status } = validationResult.data
|
||||||
|
|
||||||
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
|
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
|
||||||
|
|
||||||
@@ -142,21 +143,44 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
const template = existingTemplate[0]
|
const template = existingTemplate[0]
|
||||||
|
|
||||||
if (!template.creatorId) {
|
// Status changes require super user permission
|
||||||
logger.warn(`[${requestId}] Template ${id} has no creator, denying update`)
|
if (status !== undefined) {
|
||||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
const { verifyEffectiveSuperUser } = await import('@/lib/templates/permissions')
|
||||||
|
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
|
||||||
|
if (!effectiveSuperUser) {
|
||||||
|
logger.warn(`[${requestId}] Non-super user attempted to change template status: ${id}`)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Only super users can change template status' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { verifyCreatorPermission } = await import('@/lib/templates/permissions')
|
// For non-status updates, verify creator permission
|
||||||
const { hasPermission, error: permissionError } = await verifyCreatorPermission(
|
const hasNonStatusUpdates =
|
||||||
session.user.id,
|
name !== undefined ||
|
||||||
template.creatorId,
|
details !== undefined ||
|
||||||
'admin'
|
creatorId !== undefined ||
|
||||||
)
|
tags !== undefined ||
|
||||||
|
updateState
|
||||||
|
|
||||||
if (!hasPermission) {
|
if (hasNonStatusUpdates) {
|
||||||
logger.warn(`[${requestId}] User denied permission to update template ${id}`)
|
if (!template.creatorId) {
|
||||||
return NextResponse.json({ error: permissionError || 'Access denied' }, { status: 403 })
|
logger.warn(`[${requestId}] Template ${id} has no creator, denying update`)
|
||||||
|
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { verifyCreatorPermission } = await import('@/lib/templates/permissions')
|
||||||
|
const { hasPermission, error: permissionError } = await verifyCreatorPermission(
|
||||||
|
session.user.id,
|
||||||
|
template.creatorId,
|
||||||
|
'admin'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
logger.warn(`[${requestId}] User denied permission to update template ${id}`)
|
||||||
|
return NextResponse.json({ error: permissionError || 'Access denied' }, { status: 403 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
@@ -167,6 +191,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
if (details !== undefined) updateData.details = details
|
if (details !== undefined) updateData.details = details
|
||||||
if (tags !== undefined) updateData.tags = tags
|
if (tags !== undefined) updateData.tags = tags
|
||||||
if (creatorId !== undefined) updateData.creatorId = creatorId
|
if (creatorId !== undefined) updateData.creatorId = creatorId
|
||||||
|
if (status !== undefined) updateData.status = status
|
||||||
|
|
||||||
if (updateState && template.workflowId) {
|
if (updateState && template.workflowId) {
|
||||||
const { verifyWorkflowAccess } = await import('@/socket/middleware/permissions')
|
const { verifyWorkflowAccess } = await import('@/socket/middleware/permissions')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -95,6 +96,14 @@ export async function POST(request: NextRequest) {
|
|||||||
if (validatedData.files && validatedData.files.length > 0) {
|
if (validatedData.files && validatedData.files.length > 0) {
|
||||||
for (const file of validatedData.files) {
|
for (const file of validatedData.files) {
|
||||||
if (file.type === 'url') {
|
if (file.type === 'url') {
|
||||||
|
const urlValidation = await validateUrlWithDNS(file.data, 'fileUrl')
|
||||||
|
if (!urlValidation.isValid) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: urlValidation.error },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const filePart: FilePart = {
|
const filePart: FilePart = {
|
||||||
kind: 'file',
|
kind: 'file',
|
||||||
file: {
|
file: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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 { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||||
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,7 +40,7 @@ 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')
|
const urlValidation = await validateUrlWithDNS(validatedData.webhookUrl, 'Webhook URL')
|
||||||
if (!urlValidation.isValid) {
|
if (!urlValidation.isValid) {
|
||||||
logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error })
|
logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error })
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export async function GET(request: NextRequest) {
|
|||||||
const accessToken = searchParams.get('accessToken')
|
const accessToken = searchParams.get('accessToken')
|
||||||
const pageId = searchParams.get('pageId')
|
const pageId = searchParams.get('pageId')
|
||||||
const providedCloudId = searchParams.get('cloudId')
|
const providedCloudId = searchParams.get('cloudId')
|
||||||
const limit = searchParams.get('limit') || '25'
|
const limit = searchParams.get('limit') || '50'
|
||||||
|
const cursor = searchParams.get('cursor')
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
@@ -47,7 +48,12 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/attachments?limit=${limit}`
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(Number(limit), 250)))
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/attachments?${queryParams.toString()}`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -77,9 +83,20 @@ export async function GET(request: NextRequest) {
|
|||||||
fileSize: attachment.fileSize || 0,
|
fileSize: attachment.fileSize || 0,
|
||||||
mediaType: attachment.mediaType || '',
|
mediaType: attachment.mediaType || '',
|
||||||
downloadUrl: attachment.downloadLink || attachment._links?.download || '',
|
downloadUrl: attachment.downloadLink || attachment._links?.download || '',
|
||||||
|
status: attachment.status ?? null,
|
||||||
|
webuiUrl: attachment._links?.webui ?? null,
|
||||||
|
pageId: attachment.pageId ?? null,
|
||||||
|
blogPostId: attachment.blogPostId ?? null,
|
||||||
|
comment: attachment.comment ?? null,
|
||||||
|
version: attachment.version ?? null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return NextResponse.json({ attachments })
|
return NextResponse.json({
|
||||||
|
attachments,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error listing Confluence attachments:', error)
|
logger.error('Error listing Confluence attachments:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
285
apps/sim/app/api/tools/confluence/blogposts/route.ts
Normal file
285
apps/sim/app/api/tools/confluence/blogposts/route.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('ConfluenceBlogPostsAPI')
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
const getBlogPostSchema = z
|
||||||
|
.object({
|
||||||
|
domain: z.string().min(1, 'Domain is required'),
|
||||||
|
accessToken: z.string().min(1, 'Access token is required'),
|
||||||
|
cloudId: z.string().optional(),
|
||||||
|
blogPostId: z.string().min(1, 'Blog post ID is required'),
|
||||||
|
bodyFormat: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const validation = validateAlphanumericId(data.blogPostId, 'blogPostId', 255)
|
||||||
|
return validation.isValid
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
const validation = validateAlphanumericId(data.blogPostId, 'blogPostId', 255)
|
||||||
|
return { message: validation.error || 'Invalid blog post ID', path: ['blogPostId'] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const createBlogPostSchema = z.object({
|
||||||
|
domain: z.string().min(1, 'Domain is required'),
|
||||||
|
accessToken: z.string().min(1, 'Access token is required'),
|
||||||
|
cloudId: z.string().optional(),
|
||||||
|
spaceId: z.string().min(1, 'Space ID is required'),
|
||||||
|
title: z.string().min(1, 'Title is required'),
|
||||||
|
content: z.string().min(1, 'Content is required'),
|
||||||
|
status: z.enum(['current', 'draft']).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all blog posts or get a specific blog post
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const domain = searchParams.get('domain')
|
||||||
|
const accessToken = searchParams.get('accessToken')
|
||||||
|
const providedCloudId = searchParams.get('cloudId')
|
||||||
|
const limit = searchParams.get('limit') || '25'
|
||||||
|
const status = searchParams.get('status')
|
||||||
|
const sortOrder = searchParams.get('sort')
|
||||||
|
const cursor = searchParams.get('cursor')
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(Number(limit), 250)))
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
queryParams.append('status', status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortOrder) {
|
||||||
|
queryParams.append('sort', sortOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts?${queryParams.toString()}`
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage = errorData?.message || `Failed to list blog posts (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const blogPosts = (data.results || []).map((post: any) => ({
|
||||||
|
id: post.id,
|
||||||
|
title: post.title,
|
||||||
|
status: post.status ?? null,
|
||||||
|
spaceId: post.spaceId ?? null,
|
||||||
|
authorId: post.authorId ?? null,
|
||||||
|
createdAt: post.createdAt ?? null,
|
||||||
|
version: post.version ?? null,
|
||||||
|
webUrl: post._links?.webui ?? null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
blogPosts,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error listing blog posts:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific blog post by ID
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
// Check if this is a create or get request
|
||||||
|
if (body.title && body.content && body.spaceId) {
|
||||||
|
// Create blog post
|
||||||
|
const validation = createBlogPostSchema.safeParse(body)
|
||||||
|
if (!validation.success) {
|
||||||
|
const firstError = validation.error.errors[0]
|
||||||
|
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
cloudId: providedCloudId,
|
||||||
|
spaceId,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
status,
|
||||||
|
} = validation.data
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts`
|
||||||
|
|
||||||
|
const createBody = {
|
||||||
|
spaceId,
|
||||||
|
status: status || 'current',
|
||||||
|
title,
|
||||||
|
body: {
|
||||||
|
representation: 'storage',
|
||||||
|
value: content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(createBody),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage = errorData?.message || `Failed to create blog post (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return NextResponse.json({
|
||||||
|
id: data.id,
|
||||||
|
title: data.title,
|
||||||
|
spaceId: data.spaceId,
|
||||||
|
webUrl: data._links?.webui ?? null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Get blog post by ID
|
||||||
|
const validation = getBlogPostSchema.safeParse(body)
|
||||||
|
if (!validation.success) {
|
||||||
|
const firstError = validation.error.errors[0]
|
||||||
|
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
cloudId: providedCloudId,
|
||||||
|
blogPostId,
|
||||||
|
bodyFormat,
|
||||||
|
} = validation.data
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
if (bodyFormat) {
|
||||||
|
queryParams.append('body-format', bodyFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage = errorData?.message || `Failed to get blog post (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return NextResponse.json({
|
||||||
|
id: data.id,
|
||||||
|
title: data.title,
|
||||||
|
status: data.status ?? null,
|
||||||
|
spaceId: data.spaceId ?? null,
|
||||||
|
authorId: data.authorId ?? null,
|
||||||
|
createdAt: data.createdAt ?? null,
|
||||||
|
version: data.version ?? null,
|
||||||
|
body: data.body ?? null,
|
||||||
|
webUrl: data._links?.webui ?? null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error with blog post operation:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,8 @@ export async function GET(request: NextRequest) {
|
|||||||
const pageId = searchParams.get('pageId')
|
const pageId = searchParams.get('pageId')
|
||||||
const providedCloudId = searchParams.get('cloudId')
|
const providedCloudId = searchParams.get('cloudId')
|
||||||
const limit = searchParams.get('limit') || '25'
|
const limit = searchParams.get('limit') || '25'
|
||||||
|
const bodyFormat = searchParams.get('bodyFormat') || 'storage'
|
||||||
|
const cursor = searchParams.get('cursor')
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
@@ -130,7 +132,13 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?limit=${limit}`
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(Number(limit), 250)))
|
||||||
|
queryParams.append('body-format', bodyFormat)
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?${queryParams.toString()}`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -154,14 +162,31 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
const comments = (data.results || []).map((comment: any) => ({
|
const comments = (data.results || []).map((comment: any) => {
|
||||||
id: comment.id,
|
const bodyValue = comment.body?.storage?.value || comment.body?.view?.value || ''
|
||||||
body: comment.body?.storage?.value || comment.body?.view?.value || '',
|
return {
|
||||||
createdAt: comment.createdAt || '',
|
id: comment.id,
|
||||||
authorId: comment.authorId || '',
|
body: {
|
||||||
}))
|
value: bodyValue,
|
||||||
|
representation: bodyFormat,
|
||||||
|
},
|
||||||
|
createdAt: comment.createdAt || '',
|
||||||
|
authorId: comment.authorId || '',
|
||||||
|
status: comment.status ?? null,
|
||||||
|
title: comment.title ?? null,
|
||||||
|
pageId: comment.pageId ?? null,
|
||||||
|
blogPostId: comment.blogPostId ?? null,
|
||||||
|
parentCommentId: comment.parentCommentId ?? null,
|
||||||
|
version: comment.version ?? null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ comments })
|
return NextResponse.json({
|
||||||
|
comments,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error listing Confluence comments:', error)
|
logger.error('Error listing Confluence comments:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
cloudId: providedCloudId,
|
cloudId: providedCloudId,
|
||||||
pageId,
|
pageId,
|
||||||
labelName,
|
labelName,
|
||||||
|
prefix: labelPrefix,
|
||||||
} = await request.json()
|
} = await request.json()
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
@@ -52,12 +53,14 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels`
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/content/${pageId}/label`
|
||||||
|
|
||||||
const body = {
|
const body = [
|
||||||
prefix: 'global',
|
{
|
||||||
name: labelName,
|
prefix: labelPrefix || 'global',
|
||||||
}
|
name: labelName,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -82,7 +85,14 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return NextResponse.json({ ...data, pageId, labelName })
|
const addedLabel = data.results?.[0] || data[0] || data
|
||||||
|
return NextResponse.json({
|
||||||
|
id: addedLabel.id ?? '',
|
||||||
|
name: addedLabel.name ?? labelName,
|
||||||
|
prefix: addedLabel.prefix ?? labelPrefix ?? 'global',
|
||||||
|
pageId,
|
||||||
|
labelName,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error adding Confluence label:', error)
|
logger.error('Error adding Confluence label:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -105,6 +115,8 @@ export async function GET(request: NextRequest) {
|
|||||||
const accessToken = searchParams.get('accessToken')
|
const accessToken = searchParams.get('accessToken')
|
||||||
const pageId = searchParams.get('pageId')
|
const pageId = searchParams.get('pageId')
|
||||||
const providedCloudId = searchParams.get('cloudId')
|
const providedCloudId = searchParams.get('cloudId')
|
||||||
|
const limit = searchParams.get('limit') || '25'
|
||||||
|
const cursor = searchParams.get('cursor')
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
@@ -130,7 +142,12 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels`
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(Number(limit), 250)))
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels?${queryParams.toString()}`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -160,7 +177,12 @@ export async function GET(request: NextRequest) {
|
|||||||
prefix: label.prefix || 'global',
|
prefix: label.prefix || 'global',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return NextResponse.json({ labels })
|
return NextResponse.json({
|
||||||
|
labels,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error listing Confluence labels:', error)
|
logger.error('Error listing Confluence labels:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
96
apps/sim/app/api/tools/confluence/page-ancestors/route.ts
Normal file
96
apps/sim/app/api/tools/confluence/page-ancestors/route.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('ConfluencePageAncestorsAPI')
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ancestors (parent pages) of a specific Confluence page.
|
||||||
|
* Uses GET /wiki/api/v2/pages/{id}/ancestors
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 25 } = body
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pageId) {
|
||||||
|
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||||
|
if (!pageIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(limit, 250)))
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/ancestors?${queryParams.toString()}`
|
||||||
|
|
||||||
|
logger.info(`Fetching ancestors for page ${pageId}`)
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage = errorData?.message || `Failed to get page ancestors (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const ancestors = (data.results || []).map((page: any) => ({
|
||||||
|
id: page.id,
|
||||||
|
title: page.title,
|
||||||
|
status: page.status ?? null,
|
||||||
|
spaceId: page.spaceId ?? null,
|
||||||
|
webUrl: page._links?.webui ?? null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
ancestors,
|
||||||
|
pageId,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting page ancestors:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
104
apps/sim/app/api/tools/confluence/page-children/route.ts
Normal file
104
apps/sim/app/api/tools/confluence/page-children/route.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('ConfluencePageChildrenAPI')
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get child pages of a specific Confluence page.
|
||||||
|
* Uses GET /wiki/api/v2/pages/{id}/children
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 50, cursor } = body
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pageId) {
|
||||||
|
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||||
|
if (!pageIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(limit, 250)))
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/children?${queryParams.toString()}`
|
||||||
|
|
||||||
|
logger.info(`Fetching child pages for page ${pageId}`)
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage = errorData?.message || `Failed to get child pages (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const children = (data.results || []).map((page: any) => ({
|
||||||
|
id: page.id,
|
||||||
|
title: page.title,
|
||||||
|
status: page.status ?? null,
|
||||||
|
spaceId: page.spaceId ?? null,
|
||||||
|
childPosition: page.childPosition ?? null,
|
||||||
|
webUrl: page._links?.webui ?? null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
children,
|
||||||
|
parentId: pageId,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting child pages:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
365
apps/sim/app/api/tools/confluence/page-properties/route.ts
Normal file
365
apps/sim/app/api/tools/confluence/page-properties/route.ts
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('ConfluencePagePropertiesAPI')
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
const createPropertySchema = z.object({
|
||||||
|
domain: z.string().min(1, 'Domain is required'),
|
||||||
|
accessToken: z.string().min(1, 'Access token is required'),
|
||||||
|
cloudId: z.string().optional(),
|
||||||
|
pageId: z.string().min(1, 'Page ID is required'),
|
||||||
|
key: z.string().min(1, 'Property key is required'),
|
||||||
|
value: z.any(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatePropertySchema = z.object({
|
||||||
|
domain: z.string().min(1, 'Domain is required'),
|
||||||
|
accessToken: z.string().min(1, 'Access token is required'),
|
||||||
|
cloudId: z.string().optional(),
|
||||||
|
pageId: z.string().min(1, 'Page ID is required'),
|
||||||
|
propertyId: z.string().min(1, 'Property ID is required'),
|
||||||
|
key: z.string().min(1, 'Property key is required'),
|
||||||
|
value: z.any(),
|
||||||
|
versionNumber: z.number().min(1, 'Version number is required'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const deletePropertySchema = z.object({
|
||||||
|
domain: z.string().min(1, 'Domain is required'),
|
||||||
|
accessToken: z.string().min(1, 'Access token is required'),
|
||||||
|
cloudId: z.string().optional(),
|
||||||
|
pageId: z.string().min(1, 'Page ID is required'),
|
||||||
|
propertyId: z.string().min(1, 'Property ID is required'),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all content properties on a page.
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const domain = searchParams.get('domain')
|
||||||
|
const accessToken = searchParams.get('accessToken')
|
||||||
|
const pageId = searchParams.get('pageId')
|
||||||
|
const providedCloudId = searchParams.get('cloudId')
|
||||||
|
const limit = searchParams.get('limit') || '50'
|
||||||
|
const cursor = searchParams.get('cursor')
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pageId) {
|
||||||
|
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||||
|
if (!pageIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(Number(limit), 250)))
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties?${queryParams.toString()}`
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage =
|
||||||
|
errorData?.message || `Failed to list page properties (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const properties = (data.results || []).map((prop: any) => ({
|
||||||
|
id: prop.id,
|
||||||
|
key: prop.key,
|
||||||
|
value: prop.value ?? null,
|
||||||
|
version: prop.version ?? null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
properties,
|
||||||
|
pageId,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error listing page properties:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new content property on a page.
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
const validation = createPropertySchema.safeParse(body)
|
||||||
|
if (!validation.success) {
|
||||||
|
const firstError = validation.error.errors[0]
|
||||||
|
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { domain, accessToken, cloudId: providedCloudId, pageId, key, value } = validation.data
|
||||||
|
|
||||||
|
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||||
|
if (!pageIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties`
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ key, value }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage =
|
||||||
|
errorData?.message || `Failed to create page property (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return NextResponse.json({
|
||||||
|
id: data.id,
|
||||||
|
key: data.key,
|
||||||
|
value: data.value,
|
||||||
|
version: data.version,
|
||||||
|
pageId,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error creating page property:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a content property on a page.
|
||||||
|
*/
|
||||||
|
export async function PUT(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
const validation = updatePropertySchema.safeParse(body)
|
||||||
|
if (!validation.success) {
|
||||||
|
const firstError = validation.error.errors[0]
|
||||||
|
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
cloudId: providedCloudId,
|
||||||
|
pageId,
|
||||||
|
propertyId,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
versionNumber,
|
||||||
|
} = validation.data
|
||||||
|
|
||||||
|
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||||
|
if (!pageIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255)
|
||||||
|
if (!propertyIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties/${propertyId}`
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
version: { number: versionNumber },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage =
|
||||||
|
errorData?.message || `Failed to update page property (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return NextResponse.json({
|
||||||
|
id: data.id,
|
||||||
|
key: data.key,
|
||||||
|
value: data.value,
|
||||||
|
version: data.version,
|
||||||
|
pageId,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating page property:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a content property from a page.
|
||||||
|
*/
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
const validation = deletePropertySchema.safeParse(body)
|
||||||
|
if (!validation.success) {
|
||||||
|
const firstError = validation.error.errors[0]
|
||||||
|
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { domain, accessToken, cloudId: providedCloudId, pageId, propertyId } = validation.data
|
||||||
|
|
||||||
|
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||||
|
if (!pageIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255)
|
||||||
|
if (!propertyIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties/${propertyId}`
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage =
|
||||||
|
errorData?.message || `Failed to delete page property (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ propertyId, pageId, deleted: true })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error deleting page property:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
151
apps/sim/app/api/tools/confluence/page-versions/route.ts
Normal file
151
apps/sim/app/api/tools/confluence/page-versions/route.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('ConfluencePageVersionsAPI')
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all versions of a page or get a specific version.
|
||||||
|
* Uses GET /wiki/api/v2/pages/{id}/versions
|
||||||
|
* and GET /wiki/api/v2/pages/{page-id}/versions/{version-number}
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const {
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
pageId,
|
||||||
|
versionNumber,
|
||||||
|
cloudId: providedCloudId,
|
||||||
|
limit = 50,
|
||||||
|
cursor,
|
||||||
|
} = body
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pageId) {
|
||||||
|
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||||
|
if (!pageIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If versionNumber is provided, get specific version
|
||||||
|
if (versionNumber !== undefined && versionNumber !== null) {
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${versionNumber}`
|
||||||
|
|
||||||
|
logger.info(`Fetching version ${versionNumber} for page ${pageId}`)
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage = errorData?.message || `Failed to get page version (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
version: {
|
||||||
|
number: data.number,
|
||||||
|
message: data.message ?? null,
|
||||||
|
minorEdit: data.minorEdit ?? false,
|
||||||
|
authorId: data.authorId ?? null,
|
||||||
|
createdAt: data.createdAt ?? null,
|
||||||
|
},
|
||||||
|
pageId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// List all versions
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(limit, 250)))
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions?${queryParams.toString()}`
|
||||||
|
|
||||||
|
logger.info(`Fetching versions for page ${pageId}`)
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage = errorData?.message || `Failed to list page versions (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const versions = (data.results || []).map((version: any) => ({
|
||||||
|
number: version.number,
|
||||||
|
message: version.message ?? null,
|
||||||
|
minorEdit: version.minorEdit ?? false,
|
||||||
|
authorId: version.authorId ?? null,
|
||||||
|
createdAt: version.createdAt ?? null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
versions,
|
||||||
|
pageId,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error with page versions:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ const deletePageSchema = z
|
|||||||
accessToken: z.string().min(1, 'Access token is required'),
|
accessToken: z.string().min(1, 'Access token is required'),
|
||||||
cloudId: z.string().optional(),
|
cloudId: z.string().optional(),
|
||||||
pageId: z.string().min(1, 'Page ID is required'),
|
pageId: z.string().min(1, 'Page ID is required'),
|
||||||
|
purge: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -98,7 +99,7 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?expand=body.storage,body.view,body.atlas_doc_format`
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?body-format=storage`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -130,16 +131,18 @@ export async function POST(request: NextRequest) {
|
|||||||
id: data.id,
|
id: data.id,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
body: {
|
body: {
|
||||||
view: {
|
storage: {
|
||||||
value:
|
value: data.body?.storage?.value ?? null,
|
||||||
data.body?.storage?.value ||
|
representation: 'storage',
|
||||||
data.body?.view?.value ||
|
|
||||||
data.body?.atlas_doc_format?.value ||
|
|
||||||
data.content || // try alternative fields
|
|
||||||
data.description ||
|
|
||||||
`Content for page ${data.title}`, // fallback content
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
status: data.status ?? null,
|
||||||
|
spaceId: data.spaceId ?? null,
|
||||||
|
parentId: data.parentId ?? null,
|
||||||
|
authorId: data.authorId ?? null,
|
||||||
|
createdAt: data.createdAt ?? null,
|
||||||
|
version: data.version ?? null,
|
||||||
|
_links: data._links ?? null,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error fetching Confluence page:', error)
|
logger.error('Error fetching Confluence page:', error)
|
||||||
@@ -274,7 +277,7 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { domain, accessToken, cloudId: providedCloudId, pageId } = validation.data
|
const { domain, accessToken, cloudId: providedCloudId, pageId, purge } = validation.data
|
||||||
|
|
||||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
@@ -283,7 +286,12 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}`
|
const queryParams = new URLSearchParams()
|
||||||
|
if (purge) {
|
||||||
|
queryParams.append('purge', 'true')
|
||||||
|
}
|
||||||
|
const queryString = queryParams.toString()
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}${queryString ? `?${queryString}` : ''}`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use provided cloudId or fetch it if not provided
|
|
||||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
@@ -40,7 +39,6 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the URL with query parameters
|
|
||||||
const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages`
|
const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages`
|
||||||
const queryParams = new URLSearchParams()
|
const queryParams = new URLSearchParams()
|
||||||
|
|
||||||
@@ -57,7 +55,6 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
logger.info(`Fetching Confluence pages from: ${url}`)
|
logger.info(`Fetching Confluence pages from: ${url}`)
|
||||||
|
|
||||||
// Make the request to Confluence API with OAuth Bearer token
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -79,7 +76,6 @@ export async function POST(request: NextRequest) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Could not parse error response as JSON:', e)
|
logger.error('Could not parse error response as JSON:', e)
|
||||||
|
|
||||||
// Try to get the response text for more context
|
|
||||||
try {
|
try {
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
logger.error('Response text:', text)
|
logger.error('Response text:', text)
|
||||||
|
|||||||
120
apps/sim/app/api/tools/confluence/search-in-space/route.ts
Normal file
120
apps/sim/app/api/tools/confluence/search-in-space/route.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('ConfluenceSearchInSpaceAPI')
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for content within a specific Confluence space using CQL.
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const {
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
spaceKey,
|
||||||
|
query,
|
||||||
|
cloudId: providedCloudId,
|
||||||
|
limit = 25,
|
||||||
|
contentType,
|
||||||
|
} = body
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spaceKey) {
|
||||||
|
return NextResponse.json({ error: 'Space key is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceKeyValidation = validateAlphanumericId(spaceKey, 'spaceKey', 255)
|
||||||
|
if (!spaceKeyValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: spaceKeyValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapeCqlValue = (value: string) => value.replace(/"/g, '\\"')
|
||||||
|
|
||||||
|
let cql = `space = "${escapeCqlValue(spaceKey)}"`
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
cql += ` AND text ~ "${escapeCqlValue(query)}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType) {
|
||||||
|
cql += ` AND type = "${escapeCqlValue(contentType)}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
cql,
|
||||||
|
limit: String(Math.min(limit, 250)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/search?${searchParams.toString()}`
|
||||||
|
|
||||||
|
logger.info(`Searching in space ${spaceKey} with CQL: ${cql}`)
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage = errorData?.message || `Failed to search in space (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const results = (data.results || []).map((result: any) => ({
|
||||||
|
id: result.content?.id ?? result.id,
|
||||||
|
title: result.content?.title ?? result.title,
|
||||||
|
type: result.content?.type ?? result.type,
|
||||||
|
status: result.content?.status ?? null,
|
||||||
|
url: result.url ?? result._links?.webui ?? '',
|
||||||
|
excerpt: result.excerpt ?? '',
|
||||||
|
lastModified: result.lastModified ?? null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
results,
|
||||||
|
spaceKey,
|
||||||
|
totalSize: data.totalSize ?? results.length,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error searching in space:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,8 +42,10 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const escapeCqlValue = (value: string) => value.replace(/"/g, '\\"')
|
||||||
|
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
cql: `text ~ "${query}"`,
|
cql: `text ~ "${escapeCqlValue(query)}"`,
|
||||||
limit: limit.toString(),
|
limit: limit.toString(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -70,13 +72,27 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
const results = (data.results || []).map((result: any) => ({
|
const results = (data.results || []).map((result: any) => {
|
||||||
id: result.content?.id || result.id,
|
const spaceData = result.resultGlobalContainer || result.content?.space
|
||||||
title: result.content?.title || result.title,
|
return {
|
||||||
type: result.content?.type || result.type,
|
id: result.content?.id || result.id,
|
||||||
url: result.url || result._links?.webui || '',
|
title: result.content?.title || result.title,
|
||||||
excerpt: result.excerpt || '',
|
type: result.content?.type || result.type,
|
||||||
}))
|
url: result.url || result._links?.webui || '',
|
||||||
|
excerpt: result.excerpt || '',
|
||||||
|
status: result.content?.status ?? null,
|
||||||
|
spaceKey: result.resultGlobalContainer?.key ?? result.content?.space?.key ?? null,
|
||||||
|
space: spaceData
|
||||||
|
? {
|
||||||
|
id: spaceData.id ?? null,
|
||||||
|
key: spaceData.key ?? null,
|
||||||
|
name: spaceData.name ?? spaceData.title ?? null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
lastModified: result.lastModified ?? result.content?.history?.lastUpdated?.when ?? null,
|
||||||
|
entityType: result.entityType ?? null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ results })
|
return NextResponse.json({ results })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
124
apps/sim/app/api/tools/confluence/space-blogposts/route.ts
Normal file
124
apps/sim/app/api/tools/confluence/space-blogposts/route.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('ConfluenceSpaceBlogPostsAPI')
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all blog posts in a specific Confluence space.
|
||||||
|
* Uses GET /wiki/api/v2/spaces/{id}/blogposts
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const {
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
spaceId,
|
||||||
|
cloudId: providedCloudId,
|
||||||
|
limit = 25,
|
||||||
|
status,
|
||||||
|
bodyFormat,
|
||||||
|
cursor,
|
||||||
|
} = body
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spaceId) {
|
||||||
|
return NextResponse.json({ error: 'Space ID is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255)
|
||||||
|
if (!spaceIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(limit, 250)))
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
queryParams.append('status', status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyFormat) {
|
||||||
|
queryParams.append('body-format', bodyFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/blogposts?${queryParams.toString()}`
|
||||||
|
|
||||||
|
logger.info(`Fetching blog posts in space ${spaceId}`)
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage =
|
||||||
|
errorData?.message || `Failed to list blog posts in space (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const blogPosts = (data.results || []).map((post: any) => ({
|
||||||
|
id: post.id,
|
||||||
|
title: post.title,
|
||||||
|
status: post.status ?? null,
|
||||||
|
spaceId: post.spaceId ?? null,
|
||||||
|
authorId: post.authorId ?? null,
|
||||||
|
createdAt: post.createdAt ?? null,
|
||||||
|
version: post.version ?? null,
|
||||||
|
body: post.body ?? null,
|
||||||
|
webUrl: post._links?.webui ?? null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
blogPosts,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error listing blog posts in space:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
125
apps/sim/app/api/tools/confluence/space-pages/route.ts
Normal file
125
apps/sim/app/api/tools/confluence/space-pages/route.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('ConfluenceSpacePagesAPI')
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all pages in a specific Confluence space.
|
||||||
|
* Uses GET /wiki/api/v2/spaces/{id}/pages
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await checkSessionOrInternalAuth(request)
|
||||||
|
if (!auth.success || !auth.userId) {
|
||||||
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const {
|
||||||
|
domain,
|
||||||
|
accessToken,
|
||||||
|
spaceId,
|
||||||
|
cloudId: providedCloudId,
|
||||||
|
limit = 50,
|
||||||
|
status,
|
||||||
|
bodyFormat,
|
||||||
|
cursor,
|
||||||
|
} = body
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spaceId) {
|
||||||
|
return NextResponse.json({ error: 'Space ID is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255)
|
||||||
|
if (!spaceIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||||
|
|
||||||
|
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||||
|
if (!cloudIdValidation.isValid) {
|
||||||
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(limit, 250)))
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
queryParams.append('status', status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyFormat) {
|
||||||
|
queryParams.append('body-format', bodyFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/pages?${queryParams.toString()}`
|
||||||
|
|
||||||
|
logger.info(`Fetching pages in space ${spaceId}`)
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
logger.error('Confluence API error response:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: JSON.stringify(errorData, null, 2),
|
||||||
|
})
|
||||||
|
const errorMessage =
|
||||||
|
errorData?.message || `Failed to list pages in space (${response.status})`
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const pages = (data.results || []).map((page: any) => ({
|
||||||
|
id: page.id,
|
||||||
|
title: page.title,
|
||||||
|
status: page.status ?? null,
|
||||||
|
spaceId: page.spaceId ?? null,
|
||||||
|
parentId: page.parentId ?? null,
|
||||||
|
authorId: page.authorId ?? null,
|
||||||
|
createdAt: page.createdAt ?? null,
|
||||||
|
version: page.version ?? null,
|
||||||
|
body: page.body ?? null,
|
||||||
|
webUrl: page._links?.webui ?? null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
pages,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error listing pages in space:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message || 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const accessToken = searchParams.get('accessToken')
|
const accessToken = searchParams.get('accessToken')
|
||||||
const providedCloudId = searchParams.get('cloudId')
|
const providedCloudId = searchParams.get('cloudId')
|
||||||
const limit = searchParams.get('limit') || '25'
|
const limit = searchParams.get('limit') || '25'
|
||||||
|
const cursor = searchParams.get('cursor')
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||||
@@ -37,7 +38,12 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?limit=${limit}`
|
const queryParams = new URLSearchParams()
|
||||||
|
queryParams.append('limit', String(Math.min(Number(limit), 250)))
|
||||||
|
if (cursor) {
|
||||||
|
queryParams.append('cursor', cursor)
|
||||||
|
}
|
||||||
|
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?${queryParams.toString()}`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -67,9 +73,18 @@ export async function GET(request: NextRequest) {
|
|||||||
key: space.key,
|
key: space.key,
|
||||||
type: space.type,
|
type: space.type,
|
||||||
status: space.status,
|
status: space.status,
|
||||||
|
authorId: space.authorId ?? null,
|
||||||
|
createdAt: space.createdAt ?? null,
|
||||||
|
homepageId: space.homepageId ?? null,
|
||||||
|
description: space.description ?? null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return NextResponse.json({ spaces })
|
return NextResponse.json({
|
||||||
|
spaces,
|
||||||
|
nextCursor: data._links?.next
|
||||||
|
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||||
|
: null,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error listing Confluence spaces:', error)
|
logger.error('Error listing Confluence spaces:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ export async function POST(request: NextRequest) {
|
|||||||
formData.append('comment', comment)
|
formData.append('comment', comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add minorEdit field as required by Confluence API
|
||||||
|
formData.append('minorEdit', 'false')
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from 'zod'
|
|||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } 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 { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
|
||||||
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'
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ const DiscordSendMessageSchema = z.object({
|
|||||||
botToken: z.string().min(1, 'Bot token is required'),
|
botToken: z.string().min(1, 'Bot token is required'),
|
||||||
channelId: z.string().min(1, 'Channel ID is required'),
|
channelId: z.string().min(1, 'Channel ID is required'),
|
||||||
content: z.string().optional().nullable(),
|
content: z.string().optional().nullable(),
|
||||||
files: z.array(z.any()).optional().nullable(),
|
files: RawFileInputArraySchema.optional().nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@@ -101,6 +102,12 @@ export async function POST(request: NextRequest) {
|
|||||||
logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`)
|
logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`)
|
||||||
|
|
||||||
const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger)
|
const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger)
|
||||||
|
const filesOutput: Array<{
|
||||||
|
name: string
|
||||||
|
mimeType: string
|
||||||
|
data: string
|
||||||
|
size: number
|
||||||
|
}> = []
|
||||||
|
|
||||||
if (userFiles.length === 0) {
|
if (userFiles.length === 0) {
|
||||||
logger.warn(`[${requestId}] No valid files to upload, falling back to text-only`)
|
logger.warn(`[${requestId}] No valid files to upload, falling back to text-only`)
|
||||||
@@ -137,6 +144,12 @@ export async function POST(request: NextRequest) {
|
|||||||
logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`)
|
logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`)
|
||||||
|
|
||||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||||
|
filesOutput.push({
|
||||||
|
name: userFile.name,
|
||||||
|
mimeType: userFile.type || 'application/octet-stream',
|
||||||
|
data: buffer.toString('base64'),
|
||||||
|
size: buffer.length,
|
||||||
|
})
|
||||||
|
|
||||||
const blob = new Blob([new Uint8Array(buffer)], { type: userFile.type })
|
const blob = new Blob([new Uint8Array(buffer)], { type: userFile.type })
|
||||||
formData.append(`files[${i}]`, blob, userFile.name)
|
formData.append(`files[${i}]`, blob, userFile.name)
|
||||||
@@ -173,6 +186,7 @@ export async function POST(request: NextRequest) {
|
|||||||
message: data.content,
|
message: data.content,
|
||||||
data: data,
|
data: data,
|
||||||
fileCount: userFiles.length,
|
fileCount: userFiles.length,
|
||||||
|
files: filesOutput,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
132
apps/sim/app/api/tools/dropbox/upload/route.ts
Normal file
132
apps/sim/app/api/tools/dropbox/upload/route.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { httpHeaderSafeJson } from '@/lib/core/utils/validation'
|
||||||
|
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||||
|
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||||
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
const logger = createLogger('DropboxUploadAPI')
|
||||||
|
|
||||||
|
const DropboxUploadSchema = z.object({
|
||||||
|
accessToken: z.string().min(1, 'Access token is required'),
|
||||||
|
path: z.string().min(1, 'Destination path is required'),
|
||||||
|
file: FileInputSchema.optional().nullable(),
|
||||||
|
// Legacy field for backwards compatibility
|
||||||
|
fileContent: z.string().optional().nullable(),
|
||||||
|
fileName: z.string().optional().nullable(),
|
||||||
|
mode: z.enum(['add', 'overwrite']).optional().nullable(),
|
||||||
|
autorename: z.boolean().optional().nullable(),
|
||||||
|
mute: z.boolean().optional().nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
|
if (!authResult.success) {
|
||||||
|
logger.warn(`[${requestId}] Unauthorized Dropbox upload attempt: ${authResult.error}`)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: authResult.error || 'Authentication required' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Authenticated Dropbox upload request via ${authResult.authType}`)
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const validatedData = DropboxUploadSchema.parse(body)
|
||||||
|
|
||||||
|
let fileBuffer: Buffer
|
||||||
|
let fileName: string
|
||||||
|
|
||||||
|
// Prefer UserFile input, fall back to legacy base64 string
|
||||||
|
if (validatedData.file) {
|
||||||
|
// Process UserFile input
|
||||||
|
const userFiles = processFilesToUserFiles(
|
||||||
|
[validatedData.file as RawFileInput],
|
||||||
|
requestId,
|
||||||
|
logger
|
||||||
|
)
|
||||||
|
|
||||||
|
if (userFiles.length === 0) {
|
||||||
|
return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userFile = userFiles[0]
|
||||||
|
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
|
||||||
|
|
||||||
|
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||||
|
fileName = userFile.name
|
||||||
|
} else if (validatedData.fileContent) {
|
||||||
|
// Legacy: base64 string input (backwards compatibility)
|
||||||
|
logger.info(`[${requestId}] Using legacy base64 content input`)
|
||||||
|
fileBuffer = Buffer.from(validatedData.fileContent, 'base64')
|
||||||
|
fileName = validatedData.fileName || 'file'
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine final path
|
||||||
|
let finalPath = validatedData.path
|
||||||
|
if (finalPath.endsWith('/')) {
|
||||||
|
finalPath = `${finalPath}${fileName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Uploading to Dropbox: ${finalPath} (${fileBuffer.length} bytes)`)
|
||||||
|
|
||||||
|
const dropboxApiArg = {
|
||||||
|
path: finalPath,
|
||||||
|
mode: validatedData.mode || 'add',
|
||||||
|
autorename: validatedData.autorename ?? true,
|
||||||
|
mute: validatedData.mute ?? false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('https://content.dropboxapi.com/2/files/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Dropbox-API-Arg': httpHeaderSafeJson(dropboxApiArg),
|
||||||
|
},
|
||||||
|
body: new Uint8Array(fileBuffer),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorMessage = data.error_summary || data.error?.message || 'Failed to upload file'
|
||||||
|
logger.error(`[${requestId}] Dropbox API error:`, { status: response.status, data })
|
||||||
|
return NextResponse.json({ success: false, error: errorMessage }, { status: response.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] File uploaded successfully to ${data.path_display}`)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
output: {
|
||||||
|
file: data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
logger.warn(`[${requestId}] Validation error:`, error.errors)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: error.errors[0]?.message || 'Validation failed' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`[${requestId}] Unexpected error:`, error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user