Compare commits

...

16 Commits

Author SHA1 Message Date
Waleed Latif
5a4119d122 fix(oauth): fall back to configured scopes when DB scope is empty
Providers like Box don't return a scope field in their token response,
leaving the account.scope column empty. The credentials API now falls
back to the provider's configured scopes when the stored scope is
empty, preventing false "Additional permissions required" banners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:51:54 -07:00
Waleed Latif
2da7a34e23 fix(box): add sign_requests.readwrite scope for Box Sign operations
Box Sign API requires the sign_requests.readwrite scope in addition
to root_readwrite. Without it, sign requests fail with "The request
requires higher privileges than provided by the access token."

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:54:40 -07:00
Waleed Latif
1e16fa975d fix(box): populate OAuth scopes for Box since token response omits them
Box's OAuth2 token endpoint does not return a scope field in the
response, so Better Auth stores nothing in the DB. This causes the
credential selector to always show "Additional permissions required".
Fix by populating the scope from the requested scopes in the
account.create.before hook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:52:47 -07:00
Waleed Latif
17723bb44c style(docs): apply lint formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:30:16 -07:00
Waleed Latif
758f076d48 style(box,docusign): set block bgColor to white and regenerate docs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:20:34 -07:00
Waleed Latif
2c0ed6bca8 style(box): format chained method calls per linter rules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
faeb293ead style(docs): apply lint formatting to icon-mapping and meta.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
ea948a692e fix(box): filter empty strings from tags array in update_file
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
d3bbb51f6c refactor(box): merge Box Sign into single Box block
Combine Box and Box Sign into one unified block with all 15 operations
accessible via a single dropdown, removing the separate box_sign block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
a9ce3ea0a9 fix(box): filter empty file IDs from sourceFileIds input
Add .filter(Boolean) after splitting sourceFileIds to prevent empty
strings from trailing/double commas being sent as invalid file IDs
to the Box Sign API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
3b6900236f fix(box): rename items output to entries for list_folder_items
Rename the output field from "items" to "entries" to match Box API
naming and avoid collision with JSON schema "items" keyword that
prevented the docs generator from rendering the nested array structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
68b2302d98 fix(box): use generic output descriptions for shared file properties
Rename "Uploaded file ID/name" to "File ID/name" in
UPLOAD_FILE_OUTPUT_PROPERTIES since the constant is shared by both
upload and copy operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
6e5cd21ab0 fix(box): use canonical param ID for file normalization in params()
The params function must reference canonical IDs (params.file), not raw
subBlock IDs (uploadFile/fileRef) which are deleted after canonical
transformation. Matches the Dropbox block pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
92565c088d fix(box): remove unsupported reason param from cancel sign request
The Box Sign cancel endpoint (POST /sign_requests/{id}/cancel) does not
accept a request body per the API specification. Remove the misleading
reason parameter from the tool, types, block config, and docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
0d11f9f61c fix(box): address PR review comments
- Fix docsLink for Box Sign: use underscore (box_sign) to match docs URL
- Move normalizeFileInput from tool() to params() in Box block config to match Dropbox pattern
- Throw error on invalid additionalSigners JSON instead of silently dropping signers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
Waleed Latif
c893ec7d89 feat(box): add Box and Box Sign integrations
Add complete Box integration with file management (upload, download, get info, list folders, create/delete folders, copy, search, update metadata) and Box Sign e-signature support (create/get/list/cancel/resend sign requests). Includes OAuth provider setup, internal upload API route following the Dropbox pattern, block configurations, icon, and generated docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:09:39 -07:00
35 changed files with 3232 additions and 99 deletions

View File

@@ -1409,7 +1409,7 @@ export function AmplitudeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 49 49'>
<path
fill='#FFFFFF'
fill='currentColor'
d='M23.4,15.3c0.6,1.8,1.2,4.1,1.9,6.7c-2.6,0-5.3-0.1-7.8-0.1h-1.3c1.5-5.7,3.2-10.1,4.6-11.1 c0.1-0.1,0.2-0.1,0.4-0.1c0.2,0,0.3,0.1,0.5,0.3C21.9,11.5,22.5,12.7,23.4,15.3z M49,24.5C49,38,38,49,24.5,49S0,38,0,24.5 S11,0,24.5,0S49,11,49,24.5z M42.7,23.9c0-0.6-0.4-1.2-1-1.3l0,0l0,0l0,0c-0.1,0-0.1,0-0.2,0h-0.2c-4.1-0.3-8.4-0.4-12.4-0.5l0,0 C27,14.8,24.5,7.4,21.3,7.4c-3,0-5.8,4.9-8.2,14.5c-1.7,0-3.2,0-4.6-0.1c-0.1,0-0.2,0-0.2,0c-0.3,0-0.5,0-0.5,0 c-0.8,0.1-1.4,0.9-1.4,1.7c0,0.8,0.6,1.6,1.5,1.7l0,0h4.6c-0.4,1.9-0.8,3.8-1.1,5.6l-0.1,0.8l0,0c0,0.6,0.5,1.1,1.1,1.1 c0.4,0,0.8-0.2,1-0.5l0,0l2.2-7.1h10.7c0.8,3.1,1.7,6.3,2.8,9.3c0.6,1.6,2,5.4,4.4,5.4l0,0c3.6,0,5-5.8,5.9-9.6 c0.2-0.8,0.4-1.5,0.5-2.1l0.1-0.2l0,0c0-0.1,0-0.2,0-0.3c-0.1-0.2-0.2-0.3-0.4-0.4c-0.3-0.1-0.5,0.1-0.6,0.4l0,0l-0.1,0.2 c-0.3,0.8-0.6,1.6-0.8,2.3v0.1c-1.6,4.4-2.3,6.4-3.7,6.4l0,0l0,0l0,0c-1.8,0-3.5-7.3-4.1-10.1c-0.1-0.5-0.2-0.9-0.3-1.3h11.7 c0.2,0,0.4-0.1,0.6-0.1l0,0c0,0,0,0,0.1,0c0,0,0,0,0.1,0l0,0c0,0,0.1,0,0.1-0.1l0,0C42.5,24.6,42.7,24.3,42.7,23.9z'
/>
</svg>

View File

@@ -16,6 +16,7 @@ import {
AsanaIcon,
AshbyIcon,
AttioIcon,
BoxCompanyIcon,
BrainIcon,
BrandfetchIcon,
BrowserUseIcon,
@@ -185,6 +186,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
asana: AsanaIcon,
ashby: AshbyIcon,
attio: AttioIcon,
box: BoxCompanyIcon,
brandfetch: BrandfetchIcon,
browser_use: BrowserUseIcon,
calcom: CalComIcon,

View File

@@ -0,0 +1,440 @@
---
title: Box
description: Manage files, folders, and e-signatures with Box
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="box"
color="#FFFFFF"
/>
{/* MANUAL-CONTENT-START:intro */}
[Box](https://www.box.com/) is a leading cloud content management and file sharing platform trusted by enterprises worldwide to securely store, manage, and collaborate on files. Box provides robust APIs for automating file operations and integrating with business workflows, including [Box Sign](https://www.box.com/esignature) for native e-signatures.
With the Box integration in Sim, you can:
- **Upload files**: Upload documents, images, and other files to any Box folder
- **Download files**: Retrieve file content from Box for processing in your workflows
- **Get file info**: Access detailed metadata including size, owner, timestamps, tags, and shared links
- **List folder contents**: Browse files and folders with sorting and pagination support
- **Create folders**: Organize your Box storage by creating new folders programmatically
- **Delete files and folders**: Remove content with optional recursive deletion for folders
- **Copy files**: Duplicate files across folders with optional renaming
- **Search**: Find files and folders by name, content, extension, or location
- **Update file metadata**: Rename, move, add descriptions, or tag files
- **Create sign requests**: Send documents for e-signature with one or more signers
- **Track signing status**: Monitor the progress of sign requests
- **List sign requests**: View all sign requests with marker-based pagination
- **Cancel sign requests**: Cancel pending sign requests that are no longer needed
- **Resend sign reminders**: Send reminder notifications to signers who haven't completed signing
These capabilities allow your Sim agents to automate Box operations directly within your workflows — from organizing documents and distributing content to processing uploaded files, managing e-signature workflows for offer letters and contracts, and maintaining structured cloud storage as part of your business processes.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Box into your workflow to manage files, folders, and e-signatures. Upload and download files, search content, create folders, send documents for e-signature, track signing status, and more.
## Tools
### `box_upload_file`
Upload a file to a Box folder
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `parentFolderId` | string | Yes | The ID of the folder to upload the file to \(use "0" for root\) |
| `file` | file | No | The file to upload \(UserFile object\) |
| `fileContent` | string | No | Legacy: base64 encoded file content |
| `fileName` | string | No | Optional filename override |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | File ID |
| `name` | string | File name |
| `size` | number | File size in bytes |
| `sha1` | string | SHA1 hash of file content |
| `createdAt` | string | Creation timestamp |
| `modifiedAt` | string | Last modified timestamp |
| `parentId` | string | Parent folder ID |
| `parentName` | string | Parent folder name |
### `box_download_file`
Download a file from Box
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file to download |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | file | Downloaded file stored in execution files |
| `content` | string | Base64 encoded file content |
### `box_get_file_info`
Get detailed information about a file in Box
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file to get information about |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | File ID |
| `name` | string | File name |
| `description` | string | File description |
| `size` | number | File size in bytes |
| `sha1` | string | SHA1 hash of file content |
| `createdAt` | string | Creation timestamp |
| `modifiedAt` | string | Last modified timestamp |
| `createdBy` | object | User who created the file |
| `modifiedBy` | object | User who last modified the file |
| `ownedBy` | object | User who owns the file |
| `parentId` | string | Parent folder ID |
| `parentName` | string | Parent folder name |
| `sharedLink` | json | Shared link details |
| `tags` | array | File tags |
| `commentCount` | number | Number of comments |
### `box_list_folder_items`
List files and folders in a Box folder
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `folderId` | string | Yes | The ID of the folder to list items from \(use "0" for root\) |
| `limit` | number | No | Maximum number of items to return per page |
| `offset` | number | No | The offset for pagination |
| `sort` | string | No | Sort field: id, name, date, or size |
| `direction` | string | No | Sort direction: ASC or DESC |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `entries` | array | List of items in the folder |
| ↳ `type` | string | Item type \(file, folder, web_link\) |
| ↳ `id` | string | Item ID |
| ↳ `name` | string | Item name |
| ↳ `size` | number | Item size in bytes |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `modifiedAt` | string | Last modified timestamp |
| `totalCount` | number | Total number of items in the folder |
| `offset` | number | Current pagination offset |
| `limit` | number | Current pagination limit |
### `box_create_folder`
Create a new folder in Box
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | Yes | Name for the new folder |
| `parentFolderId` | string | Yes | The ID of the parent folder \(use "0" for root\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Folder ID |
| `name` | string | Folder name |
| `createdAt` | string | Creation timestamp |
| `modifiedAt` | string | Last modified timestamp |
| `parentId` | string | Parent folder ID |
| `parentName` | string | Parent folder name |
### `box_delete_file`
Delete a file from Box
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the file was successfully deleted |
| `message` | string | Success confirmation message |
### `box_delete_folder`
Delete a folder from Box
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `folderId` | string | Yes | The ID of the folder to delete |
| `recursive` | boolean | No | Delete folder and all its contents recursively |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the folder was successfully deleted |
| `message` | string | Success confirmation message |
### `box_copy_file`
Copy a file to another folder in Box
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file to copy |
| `parentFolderId` | string | Yes | The ID of the destination folder |
| `name` | string | No | Optional new name for the copied file |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | File ID |
| `name` | string | File name |
| `size` | number | File size in bytes |
| `sha1` | string | SHA1 hash of file content |
| `createdAt` | string | Creation timestamp |
| `modifiedAt` | string | Last modified timestamp |
| `parentId` | string | Parent folder ID |
| `parentName` | string | Parent folder name |
### `box_search`
Search for files and folders in Box
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | Yes | The search query string |
| `limit` | number | No | Maximum number of results to return |
| `offset` | number | No | The offset for pagination |
| `ancestorFolderId` | string | No | Restrict search to a specific folder and its subfolders |
| `fileExtensions` | string | No | Comma-separated file extensions to filter by \(e.g., pdf,docx\) |
| `type` | string | No | Restrict to a specific content type: file, folder, or web_link |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | array | Search results |
| ↳ `type` | string | Item type \(file, folder, web_link\) |
| ↳ `id` | string | Item ID |
| ↳ `name` | string | Item name |
| ↳ `size` | number | Item size in bytes |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `modifiedAt` | string | Last modified timestamp |
| ↳ `parentId` | string | Parent folder ID |
| ↳ `parentName` | string | Parent folder name |
| `totalCount` | number | Total number of matching results |
### `box_update_file`
Update file info in Box (rename, move, change description, add tags)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file to update |
| `name` | string | No | New name for the file |
| `description` | string | No | New description for the file \(max 256 characters\) |
| `parentFolderId` | string | No | Move the file to a different folder by specifying the folder ID |
| `tags` | string | No | Comma-separated tags to set on the file |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | File ID |
| `name` | string | File name |
| `description` | string | File description |
| `size` | number | File size in bytes |
| `sha1` | string | SHA1 hash of file content |
| `createdAt` | string | Creation timestamp |
| `modifiedAt` | string | Last modified timestamp |
| `createdBy` | object | User who created the file |
| `modifiedBy` | object | User who last modified the file |
| `ownedBy` | object | User who owns the file |
| `parentId` | string | Parent folder ID |
| `parentName` | string | Parent folder name |
| `sharedLink` | json | Shared link details |
| `tags` | array | File tags |
| `commentCount` | number | Number of comments |
### `box_sign_create_request`
Create a new Box Sign request to send documents for e-signature
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `sourceFileIds` | string | Yes | Comma-separated Box file IDs to send for signing |
| `signerEmail` | string | Yes | Primary signer email address |
| `signerRole` | string | No | Primary signer role: signer, approver, or final_copy_reader \(default: signer\) |
| `additionalSigners` | string | No | JSON array of additional signers, e.g. \[\{"email":"user@example.com","role":"signer"\}\] |
| `parentFolderId` | string | No | Box folder ID where signed documents will be stored \(default: user root\) |
| `emailSubject` | string | No | Custom subject line for the signing email |
| `emailMessage` | string | No | Custom message in the signing email body |
| `name` | string | No | Name for the sign request |
| `daysValid` | number | No | Number of days before the request expires \(0-730\) |
| `areRemindersEnabled` | boolean | No | Whether to send automatic signing reminders |
| `areTextSignaturesEnabled` | boolean | No | Whether to allow typed \(text\) signatures |
| `signatureColor` | string | No | Signature color: blue, black, or red |
| `redirectUrl` | string | No | URL to redirect signers to after signing |
| `declinedRedirectUrl` | string | No | URL to redirect signers to after declining |
| `isDocumentPreparationNeeded` | boolean | No | Whether document preparation is needed before sending |
| `externalId` | string | No | External system reference ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Sign request ID |
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
| `name` | string | Sign request name |
| `shortId` | string | Human-readable short ID |
| `signers` | array | List of signers |
| `sourceFiles` | array | Source files for signing |
| `emailSubject` | string | Custom email subject line |
| `emailMessage` | string | Custom email message body |
| `daysValid` | number | Number of days the request is valid |
| `createdAt` | string | Creation timestamp |
| `autoExpireAt` | string | Auto-expiration timestamp |
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
| `senderEmail` | string | Email of the sender |
### `box_sign_get_request`
Get the details and status of a Box Sign request
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `signRequestId` | string | Yes | The ID of the sign request to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Sign request ID |
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
| `name` | string | Sign request name |
| `shortId` | string | Human-readable short ID |
| `signers` | array | List of signers |
| `sourceFiles` | array | Source files for signing |
| `emailSubject` | string | Custom email subject line |
| `emailMessage` | string | Custom email message body |
| `daysValid` | number | Number of days the request is valid |
| `createdAt` | string | Creation timestamp |
| `autoExpireAt` | string | Auto-expiration timestamp |
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
| `senderEmail` | string | Email of the sender |
### `box_sign_list_requests`
List all Box Sign requests
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | number | No | Maximum number of sign requests to return \(max 1000\) |
| `marker` | string | No | Pagination marker from a previous response |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `signRequests` | array | List of sign requests |
| ↳ `id` | string | Sign request ID |
| ↳ `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
| ↳ `name` | string | Sign request name |
| ↳ `shortId` | string | Human-readable short ID |
| ↳ `signers` | array | List of signers |
| ↳ `sourceFiles` | array | Source files for signing |
| ↳ `emailSubject` | string | Custom email subject line |
| ↳ `emailMessage` | string | Custom email message body |
| ↳ `daysValid` | number | Number of days the request is valid |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `autoExpireAt` | string | Auto-expiration timestamp |
| ↳ `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
| ↳ `senderEmail` | string | Email of the sender |
| `count` | number | Number of sign requests returned in this page |
| `nextMarker` | string | Marker for next page of results |
### `box_sign_cancel_request`
Cancel a pending Box Sign request
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `signRequestId` | string | Yes | The ID of the sign request to cancel |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Sign request ID |
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
| `name` | string | Sign request name |
| `shortId` | string | Human-readable short ID |
| `signers` | array | List of signers |
| `sourceFiles` | array | Source files for signing |
| `emailSubject` | string | Custom email subject line |
| `emailMessage` | string | Custom email message body |
| `daysValid` | number | Number of days the request is valid |
| `createdAt` | string | Creation timestamp |
| `autoExpireAt` | string | Auto-expiration timestamp |
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
| `senderEmail` | string | Email of the sender |
### `box_sign_resend_request`
Resend a Box Sign request to signers who have not yet signed
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `signRequestId` | string | Yes | The ID of the sign request to resend |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success confirmation message |

View File

@@ -5,9 +5,9 @@ description: Send documents for e-signature via DocuSign
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
<BlockInfoCard
type="docusign"
color="#4C00FF"
color="#FFFFFF"
/>
{/* MANUAL-CONTENT-START:intro */}
@@ -26,6 +26,7 @@ With the DocuSign integration in Sim, you can:
In Sim, the DocuSign integration enables your agents to automate document workflows end-to-end. Agents can generate agreements, send them for signature, monitor completion, and retrieve signed copies—powering contract management, HR onboarding, sales closings, and compliance processes.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Create and send envelopes for e-signature, use templates, check signing status, download signed documents, and manage recipients with DocuSign.
@@ -36,194 +37,194 @@ Create and send envelopes for e-signature, use templates, check signing status,
### `docusign_send_envelope`
Create and send a DocuSign envelope with a document for e-signature
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `emailSubject` | string | Yes | Email subject for the envelope |
| `emailBody` | string | No | Email body message |
| `signerEmail` | string | Yes | Email address of the signer |
| `signerName` | string | Yes | Full name of the signer |
| `ccEmail` | string | No | Email address of carbon copy recipient |
| `ccName` | string | No | Full name of carbon copy recipient |
| `file` | file | No | Document file to send for signature |
| `status` | string | No | Envelope status: "sent" to send immediately, "created" for draft \(default: "sent"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `envelopeId` | string | Envelope ID |
| `status` | string | Envelope or operation status |
| `envelopes` | json | Array of envelopes |
| `templates` | json | Array of templates |
| `signers` | json | Array of signer recipients |
| `carbonCopies` | json | Array of CC recipients |
| `base64Content` | string | Base64-encoded document content |
| `mimeType` | string | Document MIME type |
| `fileName` | string | Document file name |
| `emailSubject` | string | Envelope email subject |
| `totalSetSize` | number | Total matching results |
| `resultSetSize` | number | Results returned |
| `envelopeId` | string | Created envelope ID |
| `status` | string | Envelope status |
| `statusDateTime` | string | Status change datetime |
| `uri` | string | Envelope URI |
### `docusign_create_from_template`
Create and send a DocuSign envelope using a pre-built template
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `templateId` | string | Yes | DocuSign template ID to use |
| `emailSubject` | string | No | Override email subject \(uses template default if not set\) |
| `emailBody` | string | No | Override email body message |
| `templateRoles` | string | Yes | JSON array of template roles, e.g. \[\{"roleName":"Signer","name":"John","email":"john@example.com"\}\] |
| `status` | string | No | Envelope status: "sent" to send immediately, "created" for draft \(default: "sent"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `envelopeId` | string | Envelope ID |
| `status` | string | Envelope or operation status |
| `envelopes` | json | Array of envelopes |
| `templates` | json | Array of templates |
| `signers` | json | Array of signer recipients |
| `carbonCopies` | json | Array of CC recipients |
| `base64Content` | string | Base64-encoded document content |
| `mimeType` | string | Document MIME type |
| `fileName` | string | Document file name |
| `emailSubject` | string | Envelope email subject |
| `totalSetSize` | number | Total matching results |
| `resultSetSize` | number | Results returned |
| `envelopeId` | string | Created envelope ID |
| `status` | string | Envelope status |
| `statusDateTime` | string | Status change datetime |
| `uri` | string | Envelope URI |
### `docusign_get_envelope`
Get the details and status of a DocuSign envelope
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `envelopeId` | string | Yes | The envelope ID to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `envelopeId` | string | Envelope ID |
| `status` | string | Envelope or operation status |
| `envelopes` | json | Array of envelopes |
| `templates` | json | Array of templates |
| `signers` | json | Array of signer recipients |
| `carbonCopies` | json | Array of CC recipients |
| `base64Content` | string | Base64-encoded document content |
| `mimeType` | string | Document MIME type |
| `fileName` | string | Document file name |
| `emailSubject` | string | Envelope email subject |
| `totalSetSize` | number | Total matching results |
| `resultSetSize` | number | Results returned |
| `status` | string | Envelope status \(created, sent, delivered, completed, declined, voided\) |
| `emailSubject` | string | Email subject line |
| `sentDateTime` | string | When the envelope was sent |
| `completedDateTime` | string | When all recipients completed signing |
| `createdDateTime` | string | When the envelope was created |
| `statusChangedDateTime` | string | When the status last changed |
| `voidedReason` | string | Reason the envelope was voided |
| `signerCount` | number | Number of signers |
| `documentCount` | number | Number of documents |
### `docusign_list_envelopes`
List envelopes from your DocuSign account with optional filters
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fromDate` | string | No | Start date filter \(ISO 8601\). Defaults to 30 days ago |
| `toDate` | string | No | End date filter \(ISO 8601\) |
| `envelopeStatus` | string | No | Filter by status: created, sent, delivered, completed, declined, voided |
| `searchText` | string | No | Search text to filter envelopes |
| `count` | string | No | Maximum number of envelopes to return \(default: 25\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `envelopeId` | string | Envelope ID |
| `status` | string | Envelope or operation status |
| `envelopes` | json | Array of envelopes |
| `templates` | json | Array of templates |
| `signers` | json | Array of signer recipients |
| `carbonCopies` | json | Array of CC recipients |
| `base64Content` | string | Base64-encoded document content |
| `mimeType` | string | Document MIME type |
| `fileName` | string | Document file name |
| `emailSubject` | string | Envelope email subject |
| `totalSetSize` | number | Total matching results |
| `resultSetSize` | number | Results returned |
| `envelopes` | array | Array of DocuSign envelopes |
| ↳ `envelopeId` | string | Unique envelope identifier |
| ↳ `status` | string | Envelope status \(created, sent, delivered, completed, declined, voided\) |
| ↳ `emailSubject` | string | Email subject line |
| ↳ `sentDateTime` | string | ISO 8601 datetime when envelope was sent |
| ↳ `completedDateTime` | string | ISO 8601 datetime when envelope was completed |
| ↳ `createdDateTime` | string | ISO 8601 datetime when envelope was created |
| ↳ `statusChangedDateTime` | string | ISO 8601 datetime of last status change |
| `totalSetSize` | number | Total number of matching envelopes |
| `resultSetSize` | number | Number of envelopes returned in this response |
### `docusign_void_envelope`
Void (cancel) a sent DocuSign envelope that has not yet been completed
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `envelopeId` | string | Yes | The envelope ID to void |
| `voidedReason` | string | Yes | Reason for voiding the envelope |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `envelopeId` | string | Envelope ID |
| `status` | string | Envelope or operation status |
| `envelopes` | json | Array of envelopes |
| `templates` | json | Array of templates |
| `signers` | json | Array of signer recipients |
| `carbonCopies` | json | Array of CC recipients |
| `base64Content` | string | Base64-encoded document content |
| `mimeType` | string | Document MIME type |
| `fileName` | string | Document file name |
| `emailSubject` | string | Envelope email subject |
| `totalSetSize` | number | Total matching results |
| `resultSetSize` | number | Results returned |
| `envelopeId` | string | Voided envelope ID |
| `status` | string | Envelope status \(voided\) |
### `docusign_download_document`
Download a signed document from a completed DocuSign envelope
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `envelopeId` | string | Yes | The envelope ID containing the document |
| `documentId` | string | No | Specific document ID to download, or "combined" for all documents merged \(default: "combined"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `envelopeId` | string | Envelope ID |
| `status` | string | Envelope or operation status |
| `envelopes` | json | Array of envelopes |
| `templates` | json | Array of templates |
| `signers` | json | Array of signer recipients |
| `carbonCopies` | json | Array of CC recipients |
| `base64Content` | string | Base64-encoded document content |
| `mimeType` | string | Document MIME type |
| `fileName` | string | Document file name |
| `emailSubject` | string | Envelope email subject |
| `totalSetSize` | number | Total matching results |
| `resultSetSize` | number | Results returned |
| `mimeType` | string | MIME type of the document |
| `fileName` | string | Original file name |
### `docusign_list_templates`
List available templates in your DocuSign account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `searchText` | string | No | Search text to filter templates by name |
| `count` | string | No | Maximum number of templates to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `envelopeId` | string | Envelope ID |
| `status` | string | Envelope or operation status |
| `envelopes` | json | Array of envelopes |
| `templates` | json | Array of templates |
| `signers` | json | Array of signer recipients |
| `carbonCopies` | json | Array of CC recipients |
| `base64Content` | string | Base64-encoded document content |
| `mimeType` | string | Document MIME type |
| `fileName` | string | Document file name |
| `emailSubject` | string | Envelope email subject |
| `totalSetSize` | number | Total matching results |
| `resultSetSize` | number | Results returned |
| `templates` | array | Array of DocuSign templates |
| ↳ `templateId` | string | Template identifier |
| ↳ `name` | string | Template name |
| ↳ `description` | string | Template description |
| ↳ `shared` | boolean | Whether template is shared |
| ↳ `created` | string | ISO 8601 creation date |
| ↳ `lastModified` | string | ISO 8601 last modified date |
| `totalSetSize` | number | Total number of matching templates |
| `resultSetSize` | number | Number of templates returned in this response |
### `docusign_list_recipients`
Get the recipient status details for a DocuSign envelope
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `envelopeId` | string | Yes | The envelope ID to get recipients for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `envelopeId` | string | Envelope ID |
| `status` | string | Envelope or operation status |
| `envelopes` | json | Array of envelopes |
| `templates` | json | Array of templates |
| `signers` | json | Array of signer recipients |
| `carbonCopies` | json | Array of CC recipients |
| `base64Content` | string | Base64-encoded document content |
| `mimeType` | string | Document MIME type |
| `fileName` | string | Document file name |
| `emailSubject` | string | Envelope email subject |
| `totalSetSize` | number | Total matching results |
| `resultSetSize` | number | Results returned |
| `signers` | array | Array of DocuSign recipients |
| ↳ `recipientId` | string | Recipient identifier |
| ↳ `name` | string | Recipient name |
| ↳ `email` | string | Recipient email address |
| ↳ `status` | string | Recipient signing status \(sent, delivered, completed, declined\) |
| ↳ `signedDateTime` | string | ISO 8601 datetime when recipient signed |
| ↳ `deliveredDateTime` | string | ISO 8601 datetime when delivered to recipient |
| `carbonCopies` | array | Array of carbon copy recipients |
| ↳ `recipientId` | string | Recipient ID |
| ↳ `name` | string | Recipient name |
| ↳ `email` | string | Recipient email |
| ↳ `status` | string | Recipient status |

View File

@@ -13,6 +13,7 @@
"asana",
"ashby",
"attio",
"box",
"brandfetch",
"browser_use",
"calcom",

View File

@@ -7,6 +7,7 @@ import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
import { getCanonicalScopesForProvider } from '@/lib/oauth/utils'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
@@ -38,7 +39,13 @@ function toCredentialResponse(
scope: string | null
) {
const storedScope = scope?.trim()
const scopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : []
// Some providers (e.g. Box) don't return scopes in their token response,
// so the DB column stays empty. Fall back to the configured scopes for
// the provider so the credential-selector doesn't show a false
// "Additional permissions required" banner.
const scopes = storedScope
? storedScope.split(/[\s,]+/).filter(Boolean)
: getCanonicalScopesForProvider(providerId)
const [_, featureType = 'default'] = providerId.split('-')
return {

View File

@@ -0,0 +1,140 @@
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 { 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('BoxUploadAPI')
const BoxUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
parentFolderId: z.string().min(1, 'Parent folder ID is required'),
file: FileInputSchema.optional().nullable(),
fileContent: z.string().optional().nullable(),
fileName: z.string().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 Box upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
logger.info(`[${requestId}] Authenticated Box upload request via ${authResult.authType}`)
const body = await request.json()
const validatedData = BoxUploadSchema.parse(body)
let fileBuffer: Buffer
let fileName: string
if (validatedData.file) {
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 = validatedData.fileName || userFile.name
} else if (validatedData.fileContent) {
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 })
}
logger.info(
`[${requestId}] Uploading to Box folder ${validatedData.parentFolderId}: ${fileName} (${fileBuffer.length} bytes)`
)
const attributes = JSON.stringify({
name: fileName,
parent: { id: validatedData.parentFolderId },
})
const formData = new FormData()
formData.append('attributes', attributes)
formData.append(
'file',
new Blob([new Uint8Array(fileBuffer)], { type: 'application/octet-stream' }),
fileName
)
const response = await fetch('https://upload.box.com/api/2.0/files/content', {
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
},
body: formData,
})
const data = await response.json()
if (!response.ok) {
const errorMessage = data.message || 'Failed to upload file'
logger.error(`[${requestId}] Box API error:`, { status: response.status, data })
return NextResponse.json({ success: false, error: errorMessage }, { status: response.status })
}
const file = data.entries?.[0]
if (!file) {
return NextResponse.json(
{ success: false, error: 'No file returned in upload response' },
{ status: 500 }
)
}
logger.info(`[${requestId}] File uploaded successfully: ${file.name} (ID: ${file.id})`)
return NextResponse.json({
success: true,
output: {
id: file.id ?? '',
name: file.name ?? '',
size: file.size ?? 0,
sha1: file.sha1 ?? null,
createdAt: file.created_at ?? null,
modifiedAt: file.modified_at ?? null,
parentId: file.parent?.id ?? null,
parentName: file.parent?.name ?? null,
},
})
} 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 }
)
}
}

View File

@@ -0,0 +1,599 @@
import { BoxCompanyIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
export const BoxBlock: BlockConfig = {
type: 'box',
name: 'Box',
description: 'Manage files, folders, and e-signatures with Box',
longDescription:
'Integrate Box into your workflow to manage files, folders, and e-signatures. Upload and download files, search content, create folders, send documents for e-signature, track signing status, and more.',
docsLink: 'https://docs.sim.ai/tools/box',
category: 'tools',
bgColor: '#FFFFFF',
icon: BoxCompanyIcon,
authMode: AuthMode.OAuth,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Upload File', id: 'upload_file' },
{ label: 'Download File', id: 'download_file' },
{ label: 'Get File Info', id: 'get_file_info' },
{ label: 'List Folder Items', id: 'list_folder_items' },
{ label: 'Create Folder', id: 'create_folder' },
{ label: 'Delete File', id: 'delete_file' },
{ label: 'Delete Folder', id: 'delete_folder' },
{ label: 'Copy File', id: 'copy_file' },
{ label: 'Search', id: 'search' },
{ label: 'Update File', id: 'update_file' },
{ label: 'Create Sign Request', id: 'sign_create_request' },
{ label: 'Get Sign Request', id: 'sign_get_request' },
{ label: 'List Sign Requests', id: 'sign_list_requests' },
{ label: 'Cancel Sign Request', id: 'sign_cancel_request' },
{ label: 'Resend Sign Request', id: 'sign_resend_request' },
],
value: () => 'upload_file',
},
{
id: 'credential',
title: 'Box Account',
type: 'oauth-input',
serviceId: 'box',
requiredScopes: getScopesForService('box'),
placeholder: 'Select Box account',
required: true,
},
// Upload File fields
{
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'file',
placeholder: 'Upload file to send to Box',
mode: 'basic',
multiple: false,
required: { field: 'operation', value: 'upload_file' },
condition: { field: 'operation', value: 'upload_file' },
},
{
id: 'fileRef',
title: 'File',
type: 'short-input',
canonicalParamId: 'file',
placeholder: 'Reference file from previous blocks',
mode: 'advanced',
required: { field: 'operation', value: 'upload_file' },
condition: { field: 'operation', value: 'upload_file' },
},
{
id: 'parentFolderId',
title: 'Parent Folder ID',
type: 'short-input',
placeholder: 'Folder ID (use "0" for root)',
required: { field: 'operation', value: ['upload_file', 'create_folder', 'copy_file'] },
condition: { field: 'operation', value: ['upload_file', 'create_folder', 'copy_file'] },
},
{
id: 'uploadFileName',
title: 'File Name',
type: 'short-input',
placeholder: 'Optional filename override',
condition: { field: 'operation', value: 'upload_file' },
mode: 'advanced',
},
// File ID field (shared by download, get info, delete, copy, update)
{
id: 'fileId',
title: 'File ID',
type: 'short-input',
placeholder: 'Box file ID',
required: {
field: 'operation',
value: ['download_file', 'get_file_info', 'delete_file', 'copy_file', 'update_file'],
},
condition: {
field: 'operation',
value: ['download_file', 'get_file_info', 'delete_file', 'copy_file', 'update_file'],
},
},
// Folder ID field (shared by list, delete folder)
{
id: 'folderId',
title: 'Folder ID',
type: 'short-input',
placeholder: 'Box folder ID (use "0" for root)',
required: { field: 'operation', value: ['list_folder_items', 'delete_folder'] },
condition: { field: 'operation', value: ['list_folder_items', 'delete_folder'] },
},
// Create Folder fields
{
id: 'folderName',
title: 'Folder Name',
type: 'short-input',
placeholder: 'Name for the new folder',
required: { field: 'operation', value: 'create_folder' },
condition: { field: 'operation', value: 'create_folder' },
},
// Copy File fields
{
id: 'copyName',
title: 'New Name',
type: 'short-input',
placeholder: 'Optional name for the copy',
condition: { field: 'operation', value: 'copy_file' },
},
// Search fields
{
id: 'query',
title: 'Search Query',
type: 'short-input',
placeholder: 'Search query string',
required: { field: 'operation', value: 'search' },
condition: { field: 'operation', value: 'search' },
},
{
id: 'ancestorFolderId',
title: 'Ancestor Folder ID',
type: 'short-input',
placeholder: 'Restrict search to a folder',
condition: { field: 'operation', value: 'search' },
mode: 'advanced',
},
{
id: 'fileExtensions',
title: 'File Extensions',
type: 'short-input',
placeholder: 'e.g., pdf,docx,xlsx',
condition: { field: 'operation', value: 'search' },
mode: 'advanced',
},
{
id: 'contentType',
title: 'Content Type',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Files', id: 'file' },
{ label: 'Folders', id: 'folder' },
{ label: 'Web Links', id: 'web_link' },
],
value: () => '',
condition: { field: 'operation', value: 'search' },
mode: 'advanced',
},
// Update File fields
{
id: 'newName',
title: 'New Name',
type: 'short-input',
placeholder: 'Rename the file',
condition: { field: 'operation', value: 'update_file' },
},
{
id: 'description',
title: 'Description',
type: 'short-input',
placeholder: 'File description (max 256 chars)',
condition: { field: 'operation', value: 'update_file' },
},
{
id: 'moveToFolderId',
title: 'Move to Folder ID',
type: 'short-input',
placeholder: 'Move file to this folder',
condition: { field: 'operation', value: 'update_file' },
mode: 'advanced',
},
{
id: 'tags',
title: 'Tags',
type: 'short-input',
placeholder: 'Comma-separated tags',
condition: { field: 'operation', value: 'update_file' },
mode: 'advanced',
},
// Delete Folder options
{
id: 'recursive',
title: 'Delete Recursively',
type: 'switch',
condition: { field: 'operation', value: 'delete_folder' },
},
// Shared pagination fields (file operations)
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Max results per page',
condition: {
field: 'operation',
value: ['list_folder_items', 'search', 'sign_list_requests'],
},
mode: 'advanced',
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
placeholder: 'Pagination offset',
condition: { field: 'operation', value: ['list_folder_items', 'search'] },
mode: 'advanced',
},
// List Folder sort options
{
id: 'sort',
title: 'Sort By',
type: 'dropdown',
options: [
{ label: 'Default', id: '' },
{ label: 'ID', id: 'id' },
{ label: 'Name', id: 'name' },
{ label: 'Date', id: 'date' },
{ label: 'Size', id: 'size' },
],
value: () => '',
condition: { field: 'operation', value: 'list_folder_items' },
mode: 'advanced',
},
{
id: 'direction',
title: 'Sort Direction',
type: 'dropdown',
options: [
{ label: 'Ascending', id: 'ASC' },
{ label: 'Descending', id: 'DESC' },
],
value: () => 'ASC',
condition: { field: 'operation', value: 'list_folder_items' },
mode: 'advanced',
},
// Sign Request fields
{
id: 'sourceFileIds',
title: 'Source File IDs',
type: 'short-input',
placeholder: 'Comma-separated Box file IDs (e.g., 12345,67890)',
required: { field: 'operation', value: 'sign_create_request' },
condition: { field: 'operation', value: 'sign_create_request' },
},
{
id: 'signerEmail',
title: 'Signer Email',
type: 'short-input',
placeholder: 'Primary signer email address',
required: { field: 'operation', value: 'sign_create_request' },
condition: { field: 'operation', value: 'sign_create_request' },
},
{
id: 'signerRole',
title: 'Signer Role',
type: 'dropdown',
options: [
{ label: 'Signer', id: 'signer' },
{ label: 'Approver', id: 'approver' },
{ label: 'Final Copy Reader', id: 'final_copy_reader' },
],
value: () => 'signer',
condition: { field: 'operation', value: 'sign_create_request' },
},
{
id: 'emailSubject',
title: 'Email Subject',
type: 'short-input',
placeholder: 'Custom email subject line',
condition: { field: 'operation', value: 'sign_create_request' },
},
{
id: 'emailMessage',
title: 'Email Message',
type: 'long-input',
placeholder: 'Custom message in the signing email',
condition: { field: 'operation', value: 'sign_create_request' },
},
{
id: 'signRequestName',
title: 'Request Name',
type: 'short-input',
placeholder: 'Name for this sign request',
condition: { field: 'operation', value: 'sign_create_request' },
},
{
id: 'additionalSigners',
title: 'Additional Signers',
type: 'long-input',
placeholder: '[{"email":"user@example.com","role":"signer"}]',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'signParentFolderId',
title: 'Destination Folder ID',
type: 'short-input',
placeholder: 'Box folder ID for signed documents',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'daysValid',
title: 'Days Valid',
type: 'short-input',
placeholder: 'Number of days before expiry (0-730)',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'areRemindersEnabled',
title: 'Enable Reminders',
type: 'switch',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'areTextSignaturesEnabled',
title: 'Allow Text Signatures',
type: 'switch',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'signatureColor',
title: 'Signature Color',
type: 'dropdown',
options: [
{ label: 'Blue', id: 'blue' },
{ label: 'Black', id: 'black' },
{ label: 'Red', id: 'red' },
],
value: () => 'blue',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'redirectUrl',
title: 'Redirect URL',
type: 'short-input',
placeholder: 'URL to redirect after signing',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'declinedRedirectUrl',
title: 'Declined Redirect URL',
type: 'short-input',
placeholder: 'URL to redirect after declining',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'isDocumentPreparationNeeded',
title: 'Document Preparation Needed',
type: 'switch',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
{
id: 'externalId',
title: 'External ID',
type: 'short-input',
placeholder: 'External system reference ID',
condition: { field: 'operation', value: 'sign_create_request' },
mode: 'advanced',
},
// Sign Request ID (shared by get, cancel, resend)
{
id: 'signRequestId',
title: 'Sign Request ID',
type: 'short-input',
placeholder: 'Box Sign request ID',
required: {
field: 'operation',
value: ['sign_get_request', 'sign_cancel_request', 'sign_resend_request'],
},
condition: {
field: 'operation',
value: ['sign_get_request', 'sign_cancel_request', 'sign_resend_request'],
},
},
// Sign list pagination marker
{
id: 'marker',
title: 'Pagination Marker',
type: 'short-input',
placeholder: 'Marker from previous response',
condition: { field: 'operation', value: 'sign_list_requests' },
mode: 'advanced',
},
],
tools: {
access: [
'box_upload_file',
'box_download_file',
'box_get_file_info',
'box_list_folder_items',
'box_create_folder',
'box_delete_file',
'box_delete_folder',
'box_copy_file',
'box_search',
'box_update_file',
'box_sign_create_request',
'box_sign_get_request',
'box_sign_list_requests',
'box_sign_cancel_request',
'box_sign_resend_request',
],
config: {
tool: (params) => {
const op = params.operation as string
if (op.startsWith('sign_')) {
return `box_${op}`
}
return `box_${op}`
},
params: (params) => {
const normalizedFile = normalizeFileInput(params.file, { single: true })
if (normalizedFile) {
params.file = normalizedFile
}
const { credential, operation, ...rest } = params
const baseParams: Record<string, unknown> = {
accessToken: credential,
}
switch (operation) {
case 'upload_file':
baseParams.parentFolderId = rest.parentFolderId
baseParams.file = rest.file
if (rest.uploadFileName) baseParams.fileName = rest.uploadFileName
break
case 'download_file':
case 'get_file_info':
case 'delete_file':
baseParams.fileId = rest.fileId
break
case 'list_folder_items':
baseParams.folderId = rest.folderId
if (rest.limit) baseParams.limit = Number(rest.limit)
if (rest.offset) baseParams.offset = Number(rest.offset)
if (rest.sort) baseParams.sort = rest.sort
if (rest.direction) baseParams.direction = rest.direction
break
case 'create_folder':
baseParams.name = rest.folderName
baseParams.parentFolderId = rest.parentFolderId
break
case 'delete_folder':
baseParams.folderId = rest.folderId
if (rest.recursive !== undefined) baseParams.recursive = rest.recursive
break
case 'copy_file':
baseParams.fileId = rest.fileId
baseParams.parentFolderId = rest.parentFolderId
if (rest.copyName) baseParams.name = rest.copyName
break
case 'search':
baseParams.query = rest.query
if (rest.limit) baseParams.limit = Number(rest.limit)
if (rest.offset) baseParams.offset = Number(rest.offset)
if (rest.ancestorFolderId) baseParams.ancestorFolderId = rest.ancestorFolderId
if (rest.fileExtensions) baseParams.fileExtensions = rest.fileExtensions
if (rest.contentType) baseParams.type = rest.contentType
break
case 'update_file':
baseParams.fileId = rest.fileId
if (rest.newName) baseParams.name = rest.newName
if (rest.description !== undefined) baseParams.description = rest.description
if (rest.moveToFolderId) baseParams.parentFolderId = rest.moveToFolderId
if (rest.tags) baseParams.tags = rest.tags
break
case 'sign_create_request':
baseParams.sourceFileIds = rest.sourceFileIds
baseParams.signerEmail = rest.signerEmail
if (rest.signerRole) baseParams.signerRole = rest.signerRole
if (rest.additionalSigners) baseParams.additionalSigners = rest.additionalSigners
if (rest.signParentFolderId) baseParams.parentFolderId = rest.signParentFolderId
if (rest.emailSubject) baseParams.emailSubject = rest.emailSubject
if (rest.emailMessage) baseParams.emailMessage = rest.emailMessage
if (rest.signRequestName) baseParams.name = rest.signRequestName
if (rest.daysValid) baseParams.daysValid = Number(rest.daysValid)
if (rest.areRemindersEnabled !== undefined)
baseParams.areRemindersEnabled = rest.areRemindersEnabled
if (rest.areTextSignaturesEnabled !== undefined)
baseParams.areTextSignaturesEnabled = rest.areTextSignaturesEnabled
if (rest.signatureColor) baseParams.signatureColor = rest.signatureColor
if (rest.redirectUrl) baseParams.redirectUrl = rest.redirectUrl
if (rest.declinedRedirectUrl) baseParams.declinedRedirectUrl = rest.declinedRedirectUrl
if (rest.isDocumentPreparationNeeded !== undefined)
baseParams.isDocumentPreparationNeeded = rest.isDocumentPreparationNeeded
if (rest.externalId) baseParams.externalId = rest.externalId
break
case 'sign_get_request':
case 'sign_cancel_request':
case 'sign_resend_request':
baseParams.signRequestId = rest.signRequestId
break
case 'sign_list_requests':
if (rest.limit) baseParams.limit = Number(rest.limit)
if (rest.marker) baseParams.marker = rest.marker
break
}
return baseParams
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Box OAuth credential' },
file: { type: 'json', description: 'File to upload (canonical param)' },
fileId: { type: 'string', description: 'Box file ID' },
folderId: { type: 'string', description: 'Box folder ID' },
parentFolderId: { type: 'string', description: 'Parent folder ID' },
query: { type: 'string', description: 'Search query' },
sourceFileIds: { type: 'string', description: 'Comma-separated Box file IDs' },
signerEmail: { type: 'string', description: 'Primary signer email address' },
signRequestId: { type: 'string', description: 'Sign request ID' },
},
outputs: {
id: 'string',
name: 'string',
description: 'string',
size: 'number',
sha1: 'string',
createdAt: 'string',
modifiedAt: 'string',
createdBy: 'json',
modifiedBy: 'json',
ownedBy: 'json',
parentId: 'string',
parentName: 'string',
sharedLink: 'json',
tags: 'json',
commentCount: 'number',
file: 'file',
content: 'string',
entries: 'json',
totalCount: 'number',
offset: 'number',
limit: 'number',
results: 'json',
deleted: 'boolean',
message: 'string',
status: 'string',
shortId: 'string',
signers: 'json',
sourceFiles: 'json',
emailSubject: 'string',
emailMessage: 'string',
daysValid: 'number',
autoExpireAt: 'string',
prepareUrl: 'string',
senderEmail: 'string',
signRequests: 'json',
count: 'number',
nextMarker: 'string',
},
}

View File

@@ -13,7 +13,7 @@ export const DocuSignBlock: BlockConfig<DocuSignResponse> = {
'Create and send envelopes for e-signature, use templates, check signing status, download signed documents, and manage recipients with DocuSign.',
docsLink: 'https://docs.sim.ai/tools/docusign',
category: 'tools',
bgColor: '#4C00FF',
bgColor: '#FFFFFF',
icon: DocuSignIcon,
authMode: AuthMode.OAuth,

View File

@@ -13,6 +13,7 @@ import { ArxivBlock } from '@/blocks/blocks/arxiv'
import { AsanaBlock } from '@/blocks/blocks/asana'
import { AshbyBlock } from '@/blocks/blocks/ashby'
import { AttioBlock } from '@/blocks/blocks/attio'
import { BoxBlock } from '@/blocks/blocks/box'
import { BrandfetchBlock } from '@/blocks/blocks/brandfetch'
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
import { CalComBlock } from '@/blocks/blocks/calcom'
@@ -216,6 +217,7 @@ export const registry: Record<string, BlockConfig> = {
ashby: AshbyBlock,
attio: AttioBlock,
brandfetch: BrandfetchBlock,
box: BoxBlock,
browser_use: BrowserUseBlock,
calcom: CalComBlock,
calendly: CalendlyBlock,

View File

@@ -211,6 +211,16 @@ export const auth = betterAuth({
modifiedAccount.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
}
// Box token response does not include a scope field, so Better Auth
// stores nothing. Populate it from the requested scopes so the
// credential-selector can verify permissions.
if (account.providerId === 'box' && !account.scope) {
const requestedScopes = getCanonicalScopesForProvider('box')
if (requestedScopes.length > 0) {
modifiedAccount.scope = requestedScopes.join(' ')
}
}
return { data: modifiedAccount }
},
after: async (account) => {
@@ -478,6 +488,7 @@ export const auth = betterAuth({
'sharepoint',
'jira',
'airtable',
'box',
'dropbox',
'salesforce',
'wealthbox',
@@ -2177,6 +2188,51 @@ export const auth = betterAuth({
},
},
{
providerId: 'box',
clientId: env.BOX_CLIENT_ID as string,
clientSecret: env.BOX_CLIENT_SECRET as string,
authorizationUrl: 'https://account.box.com/api/oauth2/authorize',
tokenUrl: 'https://api.box.com/oauth2/token',
scopes: getCanonicalScopesForProvider('box'),
responseType: 'code',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/box`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://api.box.com/2.0/users/me', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Box API error:', {
status: response.status,
statusText: response.statusText,
body: errorText,
})
throw new Error(`Box API error: ${response.status} ${response.statusText}`)
}
const data = await response.json()
return {
id: `${data.id}-${crypto.randomUUID()}`,
email: data.login,
name: data.name || data.login,
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
image: data.avatar_url || undefined,
}
} catch (error) {
logger.error('Error in Box getUserInfo:', error)
throw error
}
},
},
{
providerId: 'dropbox',
clientId: env.DROPBOX_CLIENT_ID as string,

View File

@@ -277,6 +277,8 @@ export const env = createEnv({
PIPEDRIVE_CLIENT_SECRET: z.string().optional(), // Pipedrive OAuth client secret
LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID
LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret
BOX_CLIENT_ID: z.string().optional(), // Box OAuth client ID
BOX_CLIENT_SECRET: z.string().optional(), // Box OAuth client secret
DROPBOX_CLIENT_ID: z.string().optional(), // Dropbox OAuth client ID
DROPBOX_CLIENT_SECRET: z.string().optional(), // Dropbox OAuth client secret
SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID

View File

@@ -3,6 +3,7 @@ import {
AirtableIcon,
AsanaIcon,
AttioIcon,
BoxCompanyIcon,
CalComIcon,
ConfluenceIcon,
DocuSignIcon,
@@ -572,6 +573,21 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'linear',
},
box: {
name: 'Box',
icon: BoxCompanyIcon,
services: {
box: {
name: 'Box',
description: 'Manage files, folders, and e-signatures with Box.',
providerId: 'box',
icon: BoxCompanyIcon,
baseProviderIcon: BoxCompanyIcon,
scopes: ['root_readwrite', 'sign_requests.readwrite'],
},
},
defaultService: 'box',
},
dropbox: {
name: 'Dropbox',
icon: DropboxIcon,
@@ -1125,6 +1141,15 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
useBasicAuth: false,
}
}
case 'box': {
const { clientId, clientSecret } = getCredentials(env.BOX_CLIENT_ID, env.BOX_CLIENT_SECRET)
return {
tokenEndpoint: 'https://api.box.com/oauth2/token',
clientId,
clientSecret,
useBasicAuth: false,
}
}
case 'docusign': {
const { clientId, clientSecret } = getCredentials(
env.DOCUSIGN_CLIENT_ID,

View File

@@ -21,6 +21,7 @@ export type OAuthProvider =
| 'airtable'
| 'notion'
| 'jira'
| 'box'
| 'dropbox'
| 'microsoft'
| 'microsoft-dataverse'
@@ -70,6 +71,7 @@ export type OAuthService =
| 'airtable'
| 'notion'
| 'jira'
| 'box'
| 'dropbox'
| 'microsoft-dataverse'
| 'microsoft-excel'

View File

@@ -342,6 +342,7 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
// Box scopes
root_readwrite: 'Read and write all files and folders in Box account',
root_readonly: 'Read all files and folders in Box account',
'sign_requests.readwrite': 'Create and manage Box Sign e-signature requests',
// Shopify scopes
write_products: 'Read and manage Shopify products',

View File

@@ -0,0 +1,82 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxCopyFileParams, BoxUploadFileResponse } from './types'
import { UPLOAD_FILE_OUTPUT_PROPERTIES } from './types'
export const boxCopyFileTool: ToolConfig<BoxCopyFileParams, BoxUploadFileResponse> = {
id: 'box_copy_file',
name: 'Box Copy File',
description: 'Copy a file to another folder in Box',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
fileId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the file to copy',
},
parentFolderId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the destination folder',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional new name for the copied file',
},
},
request: {
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}/copy`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
parent: { id: params.parentFolderId.trim() },
}
if (params.name) body.name = params.name
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box API error: ${response.status}`)
}
return {
success: true,
output: {
id: data.id ?? '',
name: data.name ?? '',
size: data.size ?? 0,
sha1: data.sha1 ?? null,
createdAt: data.created_at ?? null,
modifiedAt: data.modified_at ?? null,
parentId: data.parent?.id ?? null,
parentName: data.parent?.name ?? null,
},
}
},
outputs: UPLOAD_FILE_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,71 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxCreateFolderParams, BoxFolderResponse } from './types'
import { FOLDER_OUTPUT_PROPERTIES } from './types'
export const boxCreateFolderTool: ToolConfig<BoxCreateFolderParams, BoxFolderResponse> = {
id: 'box_create_folder',
name: 'Box Create Folder',
description: 'Create a new folder in Box',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name for the new folder',
},
parentFolderId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the parent folder (use "0" for root)',
},
},
request: {
url: 'https://api.box.com/2.0/folders',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
name: params.name,
parent: { id: params.parentFolderId.trim() },
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box API error: ${response.status}`)
}
return {
success: true,
output: {
id: data.id ?? '',
name: data.name ?? '',
createdAt: data.created_at ?? null,
modifiedAt: data.modified_at ?? null,
parentId: data.parent?.id ?? null,
parentName: data.parent?.name ?? null,
},
}
},
outputs: FOLDER_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,57 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { BoxDeleteFileParams } from './types'
export const boxDeleteFileTool: ToolConfig<BoxDeleteFileParams, ToolResponse> = {
id: 'box_delete_file',
name: 'Box Delete File',
description: 'Delete a file from Box',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
fileId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the file to delete',
},
},
request: {
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (response.status === 204) {
return {
success: true,
output: {
deleted: true,
message: 'File deleted successfully',
},
}
}
const data = await response.json()
throw new Error(data.message || `Box API error: ${response.status}`)
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the file was successfully deleted' },
message: { type: 'string', description: 'Success confirmation message' },
},
}

View File

@@ -0,0 +1,68 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { BoxDeleteFolderParams } from './types'
export const boxDeleteFolderTool: ToolConfig<BoxDeleteFolderParams, ToolResponse> = {
id: 'box_delete_folder',
name: 'Box Delete Folder',
description: 'Delete a folder from Box',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
folderId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the folder to delete',
},
recursive: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Delete folder and all its contents recursively',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.recursive) queryParams.set('recursive', 'true')
const qs = queryParams.toString()
return `https://api.box.com/2.0/folders/${params.folderId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (response.status === 204) {
return {
success: true,
output: {
deleted: true,
message: 'Folder deleted successfully',
},
}
}
const data = await response.json()
throw new Error(data.message || `Box API error: ${response.status}`)
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the folder was successfully deleted' },
message: { type: 'string', description: 'Success confirmation message' },
},
}

View File

@@ -0,0 +1,87 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxDownloadFileParams, BoxDownloadFileResponse } from './types'
export const boxDownloadFileTool: ToolConfig<BoxDownloadFileParams, BoxDownloadFileResponse> = {
id: 'box_download_file',
name: 'Box Download File',
description: 'Download a file from Box',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
fileId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the file to download',
},
},
request: {
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}/content`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (response.status === 202) {
const retryAfter = response.headers.get('retry-after') || 'a few'
throw new Error(`File is not yet ready for download. Retry after ${retryAfter} seconds.`)
}
if (!response.ok) {
const errorText = await response.text()
throw new Error(errorText || `Failed to download file: ${response.status}`)
}
const contentType = response.headers.get('content-type') || 'application/octet-stream'
const contentDisposition = response.headers.get('content-disposition')
let fileName = 'download'
if (contentDisposition) {
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
if (match?.[1]) {
fileName = match[1].replace(/['"]/g, '')
}
}
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
return {
success: true,
output: {
file: {
name: fileName,
mimeType: contentType,
data: buffer.toString('base64'),
size: buffer.length,
},
content: buffer.toString('base64'),
},
}
},
outputs: {
file: {
type: 'file',
description: 'Downloaded file stored in execution files',
},
content: {
type: 'string',
description: 'Base64 encoded file content',
},
},
}

View File

@@ -0,0 +1,87 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxFileInfoResponse, BoxGetFileInfoParams } from './types'
import { FILE_OUTPUT_PROPERTIES } from './types'
export const boxGetFileInfoTool: ToolConfig<BoxGetFileInfoParams, BoxFileInfoResponse> = {
id: 'box_get_file_info',
name: 'Box Get File Info',
description: 'Get detailed information about a file in Box',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
fileId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the file to get information about',
},
},
request: {
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box API error: ${response.status}`)
}
return {
success: true,
output: {
id: data.id ?? '',
name: data.name ?? '',
description: data.description ?? null,
size: data.size ?? 0,
sha1: data.sha1 ?? null,
createdAt: data.created_at ?? null,
modifiedAt: data.modified_at ?? null,
createdBy: data.created_by
? {
id: data.created_by.id,
name: data.created_by.name,
login: data.created_by.login,
}
: null,
modifiedBy: data.modified_by
? {
id: data.modified_by.id,
name: data.modified_by.name,
login: data.modified_by.login,
}
: null,
ownedBy: data.owned_by
? {
id: data.owned_by.id,
name: data.owned_by.name,
login: data.owned_by.login,
}
: null,
parentId: data.parent?.id ?? null,
parentName: data.parent?.name ?? null,
sharedLink: data.shared_link ?? null,
tags: data.tags ?? [],
commentCount: data.comment_count ?? null,
},
}
},
outputs: FILE_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,10 @@
export { boxCopyFileTool } from '@/tools/box/copy_file'
export { boxCreateFolderTool } from '@/tools/box/create_folder'
export { boxDeleteFileTool } from '@/tools/box/delete_file'
export { boxDeleteFolderTool } from '@/tools/box/delete_folder'
export { boxDownloadFileTool } from '@/tools/box/download_file'
export { boxGetFileInfoTool } from '@/tools/box/get_file_info'
export { boxListFolderItemsTool } from '@/tools/box/list_folder_items'
export { boxSearchTool } from '@/tools/box/search'
export { boxUpdateFileTool } from '@/tools/box/update_file'
export { boxUploadFileTool } from '@/tools/box/upload_file'

View File

@@ -0,0 +1,98 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxFolderItemsResponse, BoxListFolderItemsParams } from './types'
import { FOLDER_ITEMS_OUTPUT_PROPERTIES } from './types'
export const boxListFolderItemsTool: ToolConfig<BoxListFolderItemsParams, BoxFolderItemsResponse> =
{
id: 'box_list_folder_items',
name: 'Box List Folder Items',
description: 'List files and folders in a Box folder',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
folderId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the folder to list items from (use "0" for root)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of items to return per page',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'The offset for pagination',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort field: id, name, date, or size',
},
direction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort direction: ASC or DESC',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.limit !== undefined) queryParams.set('limit', String(params.limit))
if (params.offset !== undefined) queryParams.set('offset', String(params.offset))
if (params.sort) queryParams.set('sort', params.sort)
if (params.direction) queryParams.set('direction', params.direction)
const qs = queryParams.toString()
return `https://api.box.com/2.0/folders/${params.folderId.trim()}/items${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box API error: ${response.status}`)
}
return {
success: true,
output: {
entries: (data.entries ?? []).map((item: Record<string, unknown>) => ({
type: item.type ?? '',
id: item.id ?? '',
name: item.name ?? '',
size: item.size ?? null,
createdAt: item.created_at ?? null,
modifiedAt: item.modified_at ?? null,
})),
totalCount: data.total_count ?? 0,
offset: data.offset ?? 0,
limit: data.limit ?? 0,
},
}
},
outputs: FOLDER_ITEMS_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,105 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxSearchParams, BoxSearchResponse } from './types'
import { SEARCH_RESULT_OUTPUT_PROPERTIES } from './types'
export const boxSearchTool: ToolConfig<BoxSearchParams, BoxSearchResponse> = {
id: 'box_search',
name: 'Box Search',
description: 'Search for files and folders in Box',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The search query string',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'The offset for pagination',
},
ancestorFolderId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict search to a specific folder and its subfolders',
},
fileExtensions: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated file extensions to filter by (e.g., pdf,docx)',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict to a specific content type: file, folder, or web_link',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
queryParams.set('query', params.query)
if (params.limit !== undefined) queryParams.set('limit', String(params.limit))
if (params.offset !== undefined) queryParams.set('offset', String(params.offset))
if (params.ancestorFolderId)
queryParams.set('ancestor_folder_ids', params.ancestorFolderId.trim())
if (params.fileExtensions) queryParams.set('file_extensions', params.fileExtensions)
if (params.type) queryParams.set('type', params.type)
return `https://api.box.com/2.0/search?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box API error: ${response.status}`)
}
return {
success: true,
output: {
results: (data.entries ?? []).map((item: Record<string, unknown>) => ({
type: item.type ?? '',
id: item.id ?? '',
name: item.name ?? '',
size: item.size ?? null,
createdAt: item.created_at ?? null,
modifiedAt: item.modified_at ?? null,
parentId: (item.parent as Record<string, unknown> | undefined)?.id ?? null,
parentName: (item.parent as Record<string, unknown> | undefined)?.name ?? null,
})),
totalCount: data.total_count ?? 0,
},
}
},
outputs: SEARCH_RESULT_OUTPUT_PROPERTIES,
}

249
apps/sim/tools/box/types.ts Normal file
View File

@@ -0,0 +1,249 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
export interface BoxUploadFileParams {
accessToken: string
parentFolderId: string
file?: unknown
fileContent?: string
fileName?: string
}
export interface BoxDownloadFileParams {
accessToken: string
fileId: string
}
export interface BoxGetFileInfoParams {
accessToken: string
fileId: string
}
export interface BoxListFolderItemsParams {
accessToken: string
folderId: string
limit?: number
offset?: number
sort?: string
direction?: string
}
export interface BoxCreateFolderParams {
accessToken: string
name: string
parentFolderId: string
}
export interface BoxDeleteFileParams {
accessToken: string
fileId: string
}
export interface BoxDeleteFolderParams {
accessToken: string
folderId: string
recursive?: boolean
}
export interface BoxCopyFileParams {
accessToken: string
fileId: string
parentFolderId: string
name?: string
}
export interface BoxSearchParams {
accessToken: string
query: string
limit?: number
offset?: number
ancestorFolderId?: string
fileExtensions?: string
type?: string
}
export interface BoxUpdateFileParams {
accessToken: string
fileId: string
name?: string
description?: string
parentFolderId?: string
tags?: string
}
export interface BoxUploadFileResponse extends ToolResponse {
output: {
id: string
name: string
size: number
sha1: string | null
createdAt: string | null
modifiedAt: string | null
parentId: string | null
parentName: string | null
}
}
export interface BoxDownloadFileResponse extends ToolResponse {
output: {
file: {
name: string
mimeType: string
data: string
size: number
}
content: string
}
}
export interface BoxFileInfoResponse extends ToolResponse {
output: {
id: string
name: string
description: string | null
size: number
sha1: string | null
createdAt: string | null
modifiedAt: string | null
createdBy: { id: string; name: string; login: string } | null
modifiedBy: { id: string; name: string; login: string } | null
ownedBy: { id: string; name: string; login: string } | null
parentId: string | null
parentName: string | null
sharedLink: Record<string, unknown> | null
tags: string[]
commentCount: number | null
}
}
export interface BoxFolderItemsResponse extends ToolResponse {
output: {
entries: Array<Record<string, unknown>>
totalCount: number
offset: number
limit: number
}
}
export interface BoxFolderResponse extends ToolResponse {
output: {
id: string
name: string
createdAt: string | null
modifiedAt: string | null
parentId: string | null
parentName: string | null
}
}
export interface BoxSearchResponse extends ToolResponse {
output: {
results: Array<Record<string, unknown>>
totalCount: number
}
}
const USER_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'User ID' },
name: { type: 'string', description: 'User name' },
login: { type: 'string', description: 'User email/login' },
} as const satisfies Record<string, OutputProperty>
export const FILE_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'File ID' },
name: { type: 'string', description: 'File name' },
description: { type: 'string', description: 'File description', optional: true },
size: { type: 'number', description: 'File size in bytes' },
sha1: { type: 'string', description: 'SHA1 hash of file content', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
createdBy: {
type: 'object',
description: 'User who created the file',
optional: true,
properties: USER_OUTPUT_PROPERTIES,
},
modifiedBy: {
type: 'object',
description: 'User who last modified the file',
optional: true,
properties: USER_OUTPUT_PROPERTIES,
},
ownedBy: {
type: 'object',
description: 'User who owns the file',
optional: true,
properties: USER_OUTPUT_PROPERTIES,
},
parentId: { type: 'string', description: 'Parent folder ID', optional: true },
parentName: { type: 'string', description: 'Parent folder name', optional: true },
sharedLink: { type: 'json', description: 'Shared link details', optional: true },
tags: {
type: 'array',
description: 'File tags',
items: { type: 'string' },
optional: true,
},
commentCount: { type: 'number', description: 'Number of comments', optional: true },
} as const satisfies Record<string, OutputProperty>
export const FOLDER_ITEMS_OUTPUT_PROPERTIES = {
entries: {
type: 'array',
description: 'List of items in the folder',
items: {
type: 'object',
properties: {
type: { type: 'string', description: 'Item type (file, folder, web_link)' },
id: { type: 'string', description: 'Item ID' },
name: { type: 'string', description: 'Item name' },
size: { type: 'number', description: 'Item size in bytes', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
},
},
},
totalCount: { type: 'number', description: 'Total number of items in the folder' },
offset: { type: 'number', description: 'Current pagination offset' },
limit: { type: 'number', description: 'Current pagination limit' },
} as const satisfies Record<string, OutputProperty>
export const FOLDER_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'Folder ID' },
name: { type: 'string', description: 'Folder name' },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
parentId: { type: 'string', description: 'Parent folder ID', optional: true },
parentName: { type: 'string', description: 'Parent folder name', optional: true },
} as const satisfies Record<string, OutputProperty>
export const SEARCH_RESULT_OUTPUT_PROPERTIES = {
results: {
type: 'array',
description: 'Search results',
items: {
type: 'object',
properties: {
type: { type: 'string', description: 'Item type (file, folder, web_link)' },
id: { type: 'string', description: 'Item ID' },
name: { type: 'string', description: 'Item name' },
size: { type: 'number', description: 'Item size in bytes', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
parentId: { type: 'string', description: 'Parent folder ID', optional: true },
parentName: { type: 'string', description: 'Parent folder name', optional: true },
},
},
},
totalCount: { type: 'number', description: 'Total number of matching results' },
} as const satisfies Record<string, OutputProperty>
export const UPLOAD_FILE_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'File ID' },
name: { type: 'string', description: 'File name' },
size: { type: 'number', description: 'File size in bytes' },
sha1: { type: 'string', description: 'SHA1 hash of file content', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
modifiedAt: { type: 'string', description: 'Last modified timestamp', optional: true },
parentId: { type: 'string', description: 'Parent folder ID', optional: true },
parentName: { type: 'string', description: 'Parent folder name', optional: true },
} as const satisfies Record<string, OutputProperty>

View File

@@ -0,0 +1,124 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxFileInfoResponse, BoxUpdateFileParams } from './types'
import { FILE_OUTPUT_PROPERTIES } from './types'
export const boxUpdateFileTool: ToolConfig<BoxUpdateFileParams, BoxFileInfoResponse> = {
id: 'box_update_file',
name: 'Box Update File',
description: 'Update file info in Box (rename, move, change description, add tags)',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
fileId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the file to update',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New name for the file',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New description for the file (max 256 characters)',
},
parentFolderId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Move the file to a different folder by specifying the folder ID',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated tags to set on the file',
},
},
request: {
url: (params) => `https://api.box.com/2.0/files/${params.fileId.trim()}`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.name) body.name = params.name
if (params.description !== undefined) body.description = params.description
if (params.parentFolderId) body.parent = { id: params.parentFolderId.trim() }
if (params.tags)
body.tags = params.tags
.split(',')
.map((t: string) => t.trim())
.filter(Boolean)
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box API error: ${response.status}`)
}
return {
success: true,
output: {
id: data.id ?? '',
name: data.name ?? '',
description: data.description ?? null,
size: data.size ?? 0,
sha1: data.sha1 ?? null,
createdAt: data.created_at ?? null,
modifiedAt: data.modified_at ?? null,
createdBy: data.created_by
? {
id: data.created_by.id,
name: data.created_by.name,
login: data.created_by.login,
}
: null,
modifiedBy: data.modified_by
? {
id: data.modified_by.id,
name: data.modified_by.name,
login: data.modified_by.login,
}
: null,
ownedBy: data.owned_by
? {
id: data.owned_by.id,
name: data.owned_by.name,
login: data.owned_by.login,
}
: null,
parentId: data.parent?.id ?? null,
parentName: data.parent?.name ?? null,
sharedLink: data.shared_link ?? null,
tags: data.tags ?? [],
commentCount: data.comment_count ?? null,
},
}
},
outputs: FILE_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,78 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxUploadFileParams, BoxUploadFileResponse } from './types'
import { UPLOAD_FILE_OUTPUT_PROPERTIES } from './types'
export const boxUploadFileTool: ToolConfig<BoxUploadFileParams, BoxUploadFileResponse> = {
id: 'box_upload_file',
name: 'Box Upload File',
description: 'Upload a file to a Box folder',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
parentFolderId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the folder to upload the file to (use "0" for root)',
},
file: {
type: 'file',
required: false,
visibility: 'user-or-llm',
description: 'The file to upload (UserFile object)',
},
fileContent: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Legacy: base64 encoded file content',
},
fileName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional filename override',
},
},
request: {
url: '/api/tools/box/upload',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken,
parentFolderId: params.parentFolderId,
file: params.file,
fileContent: params.fileContent,
fileName: params.fileName,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to upload file')
}
return {
success: true,
output: data.output,
}
},
outputs: UPLOAD_FILE_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,80 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxSignCancelRequestParams, BoxSignResponse } from './types'
import { SIGN_REQUEST_OUTPUT_PROPERTIES } from './types'
export const boxSignCancelRequestTool: ToolConfig<BoxSignCancelRequestParams, BoxSignResponse> = {
id: 'box_sign_cancel_request',
name: 'Box Sign Cancel Request',
description: 'Cancel a pending Box Sign request',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
signRequestId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the sign request to cancel',
},
},
request: {
url: (params) => `https://api.box.com/2.0/sign_requests/${params.signRequestId}/cancel`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: () => ({}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box Sign API error: ${response.status}`)
}
return {
success: true,
output: {
id: data.id ?? '',
status: data.status ?? '',
name: data.name ?? null,
shortId: data.short_id ?? null,
signers: (data.signers ?? []).map((s: Record<string, unknown>) => ({
email: s.email ?? null,
role: s.role ?? null,
hasViewedDocument: s.has_viewed_document ?? null,
signerDecision: s.signer_decision ?? null,
embedUrl: s.embed_url ?? null,
order: s.order ?? null,
})),
sourceFiles: (data.source_files ?? []).map((f: Record<string, unknown>) => ({
id: f.id ?? null,
type: f.type ?? null,
name: f.name ?? null,
})),
emailSubject: data.email_subject ?? null,
emailMessage: data.email_message ?? null,
daysValid: data.days_valid ?? null,
createdAt: data.created_at ?? null,
autoExpireAt: data.auto_expire_at ?? null,
prepareUrl: data.prepare_url ?? null,
senderEmail: data.sender_email ?? null,
},
}
},
outputs: SIGN_REQUEST_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,225 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxSignCreateRequestParams, BoxSignResponse } from './types'
import { SIGN_REQUEST_OUTPUT_PROPERTIES } from './types'
export const boxSignCreateRequestTool: ToolConfig<BoxSignCreateRequestParams, BoxSignResponse> = {
id: 'box_sign_create_request',
name: 'Box Sign Create Request',
description: 'Create a new Box Sign request to send documents for e-signature',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
sourceFileIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated Box file IDs to send for signing',
},
signerEmail: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Primary signer email address',
},
signerRole: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Primary signer role: signer, approver, or final_copy_reader (default: signer)',
},
additionalSigners: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of additional signers, e.g. [{"email":"user@example.com","role":"signer"}]',
},
parentFolderId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Box folder ID where signed documents will be stored (default: user root)',
},
emailSubject: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom subject line for the signing email',
},
emailMessage: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom message in the signing email body',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Name for the sign request',
},
daysValid: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of days before the request expires (0-730)',
},
areRemindersEnabled: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to send automatic signing reminders',
},
areTextSignaturesEnabled: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to allow typed (text) signatures',
},
signatureColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Signature color: blue, black, or red',
},
redirectUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL to redirect signers to after signing',
},
declinedRedirectUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL to redirect signers to after declining',
},
isDocumentPreparationNeeded: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether document preparation is needed before sending',
},
externalId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'External system reference ID',
},
},
request: {
url: 'https://api.box.com/2.0/sign_requests',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const fileIds = params.sourceFileIds
.split(',')
.map((id: string) => id.trim())
.filter(Boolean)
const sourceFiles = fileIds.map((id: string) => ({ type: 'file', id }))
const signers: Array<Record<string, unknown>> = [
{
email: params.signerEmail,
role: params.signerRole || 'signer',
},
]
if (params.additionalSigners) {
try {
const additional =
typeof params.additionalSigners === 'string'
? JSON.parse(params.additionalSigners)
: params.additionalSigners
if (Array.isArray(additional)) {
signers.push(...additional)
}
} catch {
throw new Error(
'Invalid JSON in additionalSigners. Expected a JSON array of signer objects.'
)
}
}
const body: Record<string, unknown> = {
source_files: sourceFiles,
signers,
}
if (params.parentFolderId) {
body.parent_folder = { type: 'folder', id: params.parentFolderId }
}
if (params.emailSubject) body.email_subject = params.emailSubject
if (params.emailMessage) body.email_message = params.emailMessage
if (params.name) body.name = params.name
if (params.daysValid !== undefined) body.days_valid = params.daysValid
if (params.areRemindersEnabled !== undefined)
body.are_reminders_enabled = params.areRemindersEnabled
if (params.areTextSignaturesEnabled !== undefined)
body.are_text_signatures_enabled = params.areTextSignaturesEnabled
if (params.signatureColor) body.signature_color = params.signatureColor
if (params.redirectUrl) body.redirect_url = params.redirectUrl
if (params.declinedRedirectUrl) body.declined_redirect_url = params.declinedRedirectUrl
if (params.isDocumentPreparationNeeded !== undefined)
body.is_document_preparation_needed = params.isDocumentPreparationNeeded
if (params.externalId) body.external_id = params.externalId
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box Sign API error: ${response.status}`)
}
return {
success: true,
output: {
id: data.id ?? '',
status: data.status ?? '',
name: data.name ?? null,
shortId: data.short_id ?? null,
signers: (data.signers ?? []).map((s: Record<string, unknown>) => ({
email: s.email ?? null,
role: s.role ?? null,
hasViewedDocument: s.has_viewed_document ?? null,
signerDecision: s.signer_decision ?? null,
embedUrl: s.embed_url ?? null,
order: s.order ?? null,
})),
sourceFiles: (data.source_files ?? []).map((f: Record<string, unknown>) => ({
id: f.id ?? null,
type: f.type ?? null,
name: f.name ?? null,
})),
emailSubject: data.email_subject ?? null,
emailMessage: data.email_message ?? null,
daysValid: data.days_valid ?? null,
createdAt: data.created_at ?? null,
autoExpireAt: data.auto_expire_at ?? null,
prepareUrl: data.prepare_url ?? null,
senderEmail: data.sender_email ?? null,
},
}
},
outputs: SIGN_REQUEST_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,78 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxSignGetRequestParams, BoxSignResponse } from './types'
import { SIGN_REQUEST_OUTPUT_PROPERTIES } from './types'
export const boxSignGetRequestTool: ToolConfig<BoxSignGetRequestParams, BoxSignResponse> = {
id: 'box_sign_get_request',
name: 'Box Sign Get Request',
description: 'Get the details and status of a Box Sign request',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
signRequestId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the sign request to retrieve',
},
},
request: {
url: (params) => `https://api.box.com/2.0/sign_requests/${params.signRequestId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box Sign API error: ${response.status}`)
}
return {
success: true,
output: {
id: data.id ?? '',
status: data.status ?? '',
name: data.name ?? null,
shortId: data.short_id ?? null,
signers: (data.signers ?? []).map((s: Record<string, unknown>) => ({
email: s.email ?? null,
role: s.role ?? null,
hasViewedDocument: s.has_viewed_document ?? null,
signerDecision: s.signer_decision ?? null,
embedUrl: s.embed_url ?? null,
order: s.order ?? null,
})),
sourceFiles: (data.source_files ?? []).map((f: Record<string, unknown>) => ({
id: f.id ?? null,
type: f.type ?? null,
name: f.name ?? null,
})),
emailSubject: data.email_subject ?? null,
emailMessage: data.email_message ?? null,
daysValid: data.days_valid ?? null,
createdAt: data.created_at ?? null,
autoExpireAt: data.auto_expire_at ?? null,
prepareUrl: data.prepare_url ?? null,
senderEmail: data.sender_email ?? null,
},
}
},
outputs: SIGN_REQUEST_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,5 @@
export { boxSignCancelRequestTool } from '@/tools/box_sign/cancel_request'
export { boxSignCreateRequestTool } from '@/tools/box_sign/create_request'
export { boxSignGetRequestTool } from '@/tools/box_sign/get_request'
export { boxSignListRequestsTool } from '@/tools/box_sign/list_requests'
export { boxSignResendRequestTool } from '@/tools/box_sign/resend_request'

View File

@@ -0,0 +1,100 @@
import type { ToolConfig } from '@/tools/types'
import type { BoxSignListRequestsParams, BoxSignListResponse } from './types'
import { SIGN_REQUEST_LIST_OUTPUT_PROPERTIES } from './types'
export const boxSignListRequestsTool: ToolConfig<BoxSignListRequestsParams, BoxSignListResponse> = {
id: 'box_sign_list_requests',
name: 'Box Sign List Requests',
description: 'List all Box Sign requests',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of sign requests to return (max 1000)',
},
marker: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination marker from a previous response',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.limit !== undefined) queryParams.set('limit', String(params.limit))
if (params.marker) queryParams.set('marker', params.marker)
const qs = queryParams.toString()
return `https://api.box.com/2.0/sign_requests${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || `Box Sign API error: ${response.status}`)
}
const entries = data.entries ?? []
return {
success: true,
output: {
signRequests: entries.map((req: Record<string, unknown>) => ({
id: req.id ?? '',
status: req.status ?? '',
name: req.name ?? null,
shortId: req.short_id ?? null,
signers: ((req.signers as Array<Record<string, unknown>> | undefined) ?? []).map(
(s: Record<string, unknown>) => ({
email: s.email ?? null,
role: s.role ?? null,
hasViewedDocument: s.has_viewed_document ?? null,
signerDecision: s.signer_decision ?? null,
embedUrl: s.embed_url ?? null,
order: s.order ?? null,
})
),
sourceFiles: ((req.source_files as Array<Record<string, unknown>> | undefined) ?? []).map(
(f: Record<string, unknown>) => ({
id: f.id ?? null,
type: f.type ?? null,
name: f.name ?? null,
})
),
emailSubject: req.email_subject ?? null,
emailMessage: req.email_message ?? null,
daysValid: req.days_valid ?? null,
createdAt: req.created_at ?? null,
autoExpireAt: req.auto_expire_at ?? null,
prepareUrl: req.prepare_url ?? null,
senderEmail: req.sender_email ?? null,
})),
count: entries.length,
nextMarker: data.next_marker ?? null,
},
}
},
outputs: SIGN_REQUEST_LIST_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,55 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { BoxSignResendRequestParams } from './types'
export const boxSignResendRequestTool: ToolConfig<BoxSignResendRequestParams, ToolResponse> = {
id: 'box_sign_resend_request',
name: 'Box Sign Resend Request',
description: 'Resend a Box Sign request to signers who have not yet signed',
version: '1.0.0',
oauth: {
required: true,
provider: 'box',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Box API',
},
signRequestId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the sign request to resend',
},
},
request: {
url: (params) => `https://api.box.com/2.0/sign_requests/${params.signRequestId}/resend`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json()
throw new Error(data.message || `Box Sign API error: ${response.status}`)
}
return {
success: true,
output: {
message: 'Sign request resent successfully',
},
}
},
outputs: {
message: { type: 'string', description: 'Success confirmation message' },
},
}

View File

@@ -0,0 +1,162 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
export interface BoxSignCreateRequestParams {
accessToken: string
sourceFileIds: string
signerEmail: string
signerRole?: string
additionalSigners?: string
parentFolderId?: string
emailSubject?: string
emailMessage?: string
name?: string
daysValid?: number
areRemindersEnabled?: boolean
areTextSignaturesEnabled?: boolean
signatureColor?: string
redirectUrl?: string
declinedRedirectUrl?: string
isDocumentPreparationNeeded?: boolean
externalId?: string
}
export interface BoxSignGetRequestParams {
accessToken: string
signRequestId: string
}
export interface BoxSignListRequestsParams {
accessToken: string
limit?: number
marker?: string
}
export interface BoxSignCancelRequestParams {
accessToken: string
signRequestId: string
}
export interface BoxSignResendRequestParams {
accessToken: string
signRequestId: string
}
export interface BoxSignResponse extends ToolResponse {
output: {
id: string
status: string
name: string | null
shortId: string | null
signers: Array<Record<string, unknown>>
sourceFiles: Array<Record<string, unknown>>
emailSubject: string | null
emailMessage: string | null
daysValid: number | null
createdAt: string | null
autoExpireAt: string | null
prepareUrl: string | null
senderEmail: string | null
}
}
export interface BoxSignListResponse extends ToolResponse {
output: {
signRequests: Array<Record<string, unknown>>
count: number
nextMarker: string | null
}
}
const SIGNER_OUTPUT_PROPERTIES = {
email: { type: 'string', description: 'Signer email address' },
role: { type: 'string', description: 'Signer role (signer, approver, final_copy_reader)' },
hasViewedDocument: {
type: 'boolean',
description: 'Whether the signer has viewed the document',
optional: true,
},
signerDecision: {
type: 'json',
description: 'Signer decision details (type, finalized_at, additional_info)',
optional: true,
},
embedUrl: {
type: 'string',
description: 'URL for embedded signing experience',
optional: true,
},
order: { type: 'number', description: 'Order in signing sequence', optional: true },
} as const satisfies Record<string, OutputProperty>
const SOURCE_FILE_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'File ID' },
type: { type: 'string', description: 'File type' },
name: { type: 'string', description: 'File name', optional: true },
} as const satisfies Record<string, OutputProperty>
export const SIGN_REQUEST_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'Sign request ID' },
status: {
type: 'string',
description:
'Request status (converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing)',
},
name: { type: 'string', description: 'Sign request name', optional: true },
shortId: { type: 'string', description: 'Human-readable short ID', optional: true },
signers: {
type: 'array',
description: 'List of signers',
items: {
type: 'object',
properties: SIGNER_OUTPUT_PROPERTIES,
},
},
sourceFiles: {
type: 'array',
description: 'Source files for signing',
items: {
type: 'object',
properties: SOURCE_FILE_OUTPUT_PROPERTIES,
},
},
emailSubject: {
type: 'string',
description: 'Custom email subject line',
optional: true,
},
emailMessage: {
type: 'string',
description: 'Custom email message body',
optional: true,
},
daysValid: {
type: 'number',
description: 'Number of days the request is valid',
optional: true,
},
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
autoExpireAt: { type: 'string', description: 'Auto-expiration timestamp', optional: true },
prepareUrl: {
type: 'string',
description: 'URL for document preparation (if preparation is needed)',
optional: true,
},
senderEmail: { type: 'string', description: 'Email of the sender', optional: true },
} as const satisfies Record<string, OutputProperty>
export const SIGN_REQUEST_LIST_OUTPUT_PROPERTIES = {
signRequests: {
type: 'array',
description: 'List of sign requests',
items: {
type: 'object',
properties: SIGN_REQUEST_OUTPUT_PROPERTIES,
},
},
count: { type: 'number', description: 'Number of sign requests returned in this page' },
nextMarker: {
type: 'string',
description: 'Marker for next page of results',
optional: true,
},
} as const satisfies Record<string, OutputProperty>

View File

@@ -153,6 +153,25 @@ import {
attioUpdateTaskTool,
attioUpdateWebhookTool,
} from '@/tools/attio'
import {
boxCopyFileTool,
boxCreateFolderTool,
boxDeleteFileTool,
boxDeleteFolderTool,
boxDownloadFileTool,
boxGetFileInfoTool,
boxListFolderItemsTool,
boxSearchTool,
boxUpdateFileTool,
boxUploadFileTool,
} from '@/tools/box'
import {
boxSignCancelRequestTool,
boxSignCreateRequestTool,
boxSignGetRequestTool,
boxSignListRequestsTool,
boxSignResendRequestTool,
} from '@/tools/box_sign'
import { brandfetchGetBrandTool, brandfetchSearchTool } from '@/tools/brandfetch'
import { browserUseRunTaskTool } from '@/tools/browser_use'
import {
@@ -2409,6 +2428,21 @@ export const tools: Record<string, ToolConfig> = {
ashby_update_candidate: ashbyUpdateCandidateTool,
brandfetch_get_brand: brandfetchGetBrandTool,
brandfetch_search: brandfetchSearchTool,
box_copy_file: boxCopyFileTool,
box_create_folder: boxCreateFolderTool,
box_delete_file: boxDeleteFileTool,
box_delete_folder: boxDeleteFolderTool,
box_download_file: boxDownloadFileTool,
box_get_file_info: boxGetFileInfoTool,
box_list_folder_items: boxListFolderItemsTool,
box_search: boxSearchTool,
box_update_file: boxUpdateFileTool,
box_upload_file: boxUploadFileTool,
box_sign_create_request: boxSignCreateRequestTool,
box_sign_get_request: boxSignGetRequestTool,
box_sign_list_requests: boxSignListRequestsTool,
box_sign_cancel_request: boxSignCancelRequestTool,
box_sign_resend_request: boxSignResendRequestTool,
browser_use_run_task: browserUseRunTaskTool,
openai_embeddings: openAIEmbeddingsTool,
http_request: httpRequestTool,