diff --git a/apps/docs/content/docs/en/integrations/agentmail.mdx b/apps/docs/content/docs/en/integrations/agentmail.mdx index ebdcb8714af..e3bbdb376d6 100644 --- a/apps/docs/content/docs/en/integrations/agentmail.mdx +++ b/apps/docs/content/docs/en/integrations/agentmail.mdx @@ -142,7 +142,7 @@ Delete an email inbox in AgentMail ### Delete Thread -Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash) +Permanently delete an email thread in AgentMail and all of its messages. This cannot be undone. #### Input @@ -151,7 +151,6 @@ Delete an email thread in AgentMail (moves to trash, or permanently deletes if a | `apiKey` | string | Yes | AgentMail API key | | `inboxId` | string | Yes | ID of the inbox containing the thread | | `threadId` | string | Yes | ID of the thread to delete | -| `permanent` | boolean | No | Force permanent deletion instead of moving to trash | #### Output diff --git a/apps/docs/content/docs/en/integrations/apify.mdx b/apps/docs/content/docs/en/integrations/apify.mdx index c866477c686..efa2962b187 100644 --- a/apps/docs/content/docs/en/integrations/apify.mdx +++ b/apps/docs/content/docs/en/integrations/apify.mdx @@ -48,17 +48,15 @@ Run an APIFY actor synchronously and get results (max 5 minutes) | `actorId` | string | Yes | Actor ID or username/actor-name. Examples: "apify/web-scraper", "janedoe/my-actor", "moJRLRc85AitArpNN" | | `input` | string | No | Actor input as JSON string. Example: \{"startUrls": \[\{"url": "https://example.com"\}\], "maxPages": 10\} | | `memory` | number | No | Memory in megabytes allocated for the actor run \(128-32768\). Example: 1024 for 1GB, 2048 for 2GB | -| `timeout` | number | No | Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour | +| `actorTimeout` | number | No | Timeout in seconds for the actor run. Use 0 for no timeout. Example: 300 for 5 minutes | | `build` | string | No | Actor build to run. Examples: "latest", "beta", "1.2.3", "build-tag-name" | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Whether the actor run succeeded | -| `runId` | string | APIFY run ID | -| `status` | string | Run status \(SUCCEEDED, FAILED, etc.\) | -| `items` | array | Dataset items \(if completed\) | +| `success` | boolean | Whether the request returned dataset items \(not the run's own terminal status\) | +| `items` | array | Dataset items produced by the run | ### APIFY Run Actor (Async) @@ -74,7 +72,7 @@ Run an APIFY actor asynchronously with polling for long-running tasks | `waitForFinish` | number | No | Initial wait time in seconds \(0-60\) before polling starts. Example: 30 | | `itemLimit` | number | No | Max dataset items to fetch \(1-250000\). Default: 100. Example: 500 | | `memory` | number | No | Memory in megabytes allocated for the actor run \(128-32768\). Example: 1024 for 1GB, 2048 for 2GB | -| `timeout` | number | No | Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour | +| `actorTimeout` | number | No | Timeout in seconds for the actor run. Use 0 for no timeout. Example: 300 for 5 minutes | | `build` | string | No | Actor build to run. Examples: "latest", "beta", "1.2.3", "build-tag-name" | #### Output @@ -100,15 +98,14 @@ Run a saved APIFY actor task synchronously and get dataset items (max 5 minutes) | `input` | string | No | JSON string that overrides the task\'s saved input. Example: \{"startUrls": \[\{"url": "https://example.com"\}\]\} | | `itemLimit` | number | No | Max dataset items to return \(1-250000\). Example: 500 | | `memory` | number | No | Memory in megabytes allocated for the run \(128-32768\). Example: 1024 for 1GB | -| `timeout` | number | No | Timeout in seconds for the run. Example: 300 for 5 minutes | +| `taskTimeout` | number | No | Timeout in seconds for the run. Use 0 for no timeout. Example: 300 for 5 minutes | | `build` | string | No | Actor build to run. Examples: "latest", "beta", "1.2.3" | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Whether the task run succeeded | -| `status` | string | Run status \(SUCCEEDED, FAILED, etc.\) | +| `success` | boolean | Whether the request returned dataset items \(not the run's own terminal status\) | | `items` | array | Dataset items produced by the run | ### APIFY Get Dataset Items diff --git a/apps/docs/content/docs/en/integrations/attio.mdx b/apps/docs/content/docs/en/integrations/attio.mdx index 4e180bc9559..940f6a652ab 100644 --- a/apps/docs/content/docs/en/integrations/attio.mdx +++ b/apps/docs/content/docs/en/integrations/attio.mdx @@ -1092,7 +1092,7 @@ List the attributes (schema fields) defined on an Attio object or list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `target` | string | Yes | Whether the attributes belong to an object or a list: objects or lists | +| `target` | string | Yes | Which collection the attribute belongs to. Must be exactly 'objects' or 'lists'. | | `identifier` | string | Yes | The object or list ID or slug \(e.g. people, companies\) | | `limit` | number | No | Maximum number of attributes to return | | `offset` | number | No | Number of attributes to skip for pagination | @@ -1129,7 +1129,7 @@ Get a single attribute (schema field) on an Attio object or list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `target` | string | Yes | Whether the attribute belongs to an object or a list: objects or lists | +| `target` | string | Yes | Which collection the attribute belongs to. Must be exactly 'objects' or 'lists'. | | `identifier` | string | Yes | The object or list ID or slug \(e.g. people, companies\) | | `attribute` | string | Yes | The attribute ID or slug | @@ -1162,7 +1162,7 @@ Create a new attribute (schema field) on an Attio object or list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `target` | string | Yes | Whether to create the attribute on an object or a list: objects or lists | +| `target` | string | Yes | Which collection the attribute belongs to. Must be exactly 'objects' or 'lists'. | | `identifier` | string | Yes | The object or list ID or slug \(e.g. people, companies\) | | `title` | string | Yes | The attribute display title | | `apiSlug` | string | Yes | The attribute API slug \(unique, snake_case\) | @@ -1202,7 +1202,7 @@ Update an attribute (schema field) on an Attio object or list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `target` | string | Yes | Whether the attribute belongs to an object or a list: objects or lists | +| `target` | string | Yes | Which collection the attribute belongs to. Must be exactly 'objects' or 'lists'. | | `identifier` | string | Yes | The object or list ID or slug \(e.g. people, companies\) | | `attribute` | string | Yes | The attribute ID or slug to update | | `title` | string | No | New attribute display title | diff --git a/apps/docs/content/docs/en/integrations/box.mdx b/apps/docs/content/docs/en/integrations/box.mdx index 630c50a1152..38a8bdc733a 100644 --- a/apps/docs/content/docs/en/integrations/box.mdx +++ b/apps/docs/content/docs/en/integrations/box.mdx @@ -52,7 +52,6 @@ Upload a file to a Box folder | --------- | ---- | -------- | ----------- | | `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 @@ -320,7 +319,7 @@ Create a new Box Sign request to send documents for e-signature | 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\) | +| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, error, error_converting, error_sending, expired, finalizing, error_finalizing\) | | `name` | string | Sign request name | | `shortId` | string | Human-readable short ID | | `signers` | array | List of signers | @@ -348,7 +347,7 @@ Get the details and status of a Box Sign request | 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\) | +| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, error, error_converting, error_sending, expired, finalizing, error_finalizing\) | | `name` | string | Sign request name | | `shortId` | string | Human-readable short ID | | `signers` | array | List of signers | @@ -378,7 +377,7 @@ List all Box Sign requests | --------- | ---- | ----------- | | `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\) | +| ↳ `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, error, error_converting, error_sending, expired, finalizing, error_finalizing\) | | ↳ `name` | string | Sign request name | | ↳ `shortId` | string | Human-readable short ID | | ↳ `signers` | array | List of signers | @@ -408,7 +407,7 @@ Cancel a pending Box Sign request | 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\) | +| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, error, error_converting, error_sending, expired, finalizing, error_finalizing\) | | `name` | string | Sign request name | | `shortId` | string | Human-readable short ID | | `signers` | array | List of signers | diff --git a/apps/docs/content/docs/en/integrations/calcom.mdx b/apps/docs/content/docs/en/integrations/calcom.mdx index 5009987e2c1..0c74ce3bea9 100644 --- a/apps/docs/content/docs/en/integrations/calcom.mdx +++ b/apps/docs/content/docs/en/integrations/calcom.mdx @@ -44,7 +44,6 @@ Create a new booking on Cal.com | --------- | ---- | -------- | ----------- | | `eventTypeId` | number | Yes | The ID of the event type to book | | `start` | string | Yes | Start time in UTC ISO 8601 format \(e.g., 2024-01-15T09:00:00Z\) | -| `attendee` | object | Yes | Attendee information object with name, email, timeZone, and optional phoneNumber \(constructed from individual attendee fields\) | | `guests` | array | No | Array of guest email addresses | | `lengthInMinutes` | number | No | Duration of the booking in minutes \(overrides event type default\) | | `metadata` | object | No | Custom metadata to attach to the booking | diff --git a/apps/docs/content/docs/en/integrations/daytona.mdx b/apps/docs/content/docs/en/integrations/daytona.mdx index 0bd13fc5ec2..9ed7a15b62b 100644 --- a/apps/docs/content/docs/en/integrations/daytona.mdx +++ b/apps/docs/content/docs/en/integrations/daytona.mdx @@ -172,14 +172,14 @@ Execute a shell command inside a Daytona sandbox | `command` | string | Yes | Shell command to execute | | `cwd` | string | No | Working directory for the command \(defaults to the sandbox working directory\) | | `env` | json | No | Environment variables to set for the command as key-value pairs | -| `timeout` | number | No | Timeout in seconds \(defaults to 10 seconds\) | +| `commandTimeout` | number | No | Maximum time in seconds to wait for the command to complete; 0 disables the server-side limit. Daytona documents no default, so leaving this blank uses whatever the toolbox daemon applies. | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `exitCode` | number | Exit code of the command \(-1 if missing from the response\) | -| `result` | string | Combined stdout/stderr output of the command | +| `result` | string | Standard output from the command | ### Daytona Run Code @@ -194,14 +194,14 @@ Run Python, JavaScript, or TypeScript code inside a Daytona sandbox | `code` | string | Yes | Code to run | | `language` | string | Yes | Language of the code: python, javascript, or typescript | | `env` | json | No | Environment variables to set for the run as key-value pairs | -| `timeout` | number | No | Timeout in seconds \(defaults to 10 seconds\) | +| `runTimeout` | number | No | Maximum time in seconds to wait for the code run to complete; 0 disables the server-side limit. Daytona documents no default, so leaving this blank uses whatever the toolbox daemon applies. | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `exitCode` | number | Exit code of the code run \(-1 if missing from the response\) | -| `result` | string | Combined stdout/stderr output of the code run | +| `result` | string | Standard output from the code \(same as artifacts.stdout\) | | `artifacts` | json | Artifacts produced by the run \(e.g., matplotlib charts\) | ### Daytona Upload File @@ -216,7 +216,6 @@ Upload a file to a Daytona sandbox | `sandboxId` | string | Yes | ID of the sandbox to upload the file to | | `destinationPath` | string | Yes | Destination path in the sandbox \(a trailing slash uploads into that directory using the file name\) | | `file` | file | No | The file to upload | -| `fileContent` | string | No | Legacy: base64 encoded file content | | `fileName` | string | No | Optional file name override | #### Output diff --git a/apps/docs/content/docs/en/integrations/discord.mdx b/apps/docs/content/docs/en/integrations/discord.mdx index f2607f3cf20..df257499174 100644 --- a/apps/docs/content/docs/en/integrations/discord.mdx +++ b/apps/docs/content/docs/en/integrations/discord.mdx @@ -264,7 +264,7 @@ Remove a reaction from a Discord message | `channelId` | string | Yes | The Discord channel ID containing the message, e.g., 123456789012345678 | | `messageId` | string | Yes | The ID of the message with the reaction, e.g., 123456789012345678 | | `emoji` | string | Yes | The emoji to remove \(unicode emoji or custom emoji in name:id format\) | -| `userId` | string | No | The user ID whose reaction to remove \(omit to remove bot's own reaction\), e.g., 123456789012345678 | +| `userId` | string | No | The user ID whose reaction to remove \(omit, or pass "@me", to remove the bot\'s own reaction\), e.g., 123456789012345678 | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | #### Output diff --git a/apps/docs/content/docs/en/integrations/dropbox.mdx b/apps/docs/content/docs/en/integrations/dropbox.mdx index 83453f4cb9c..27b95131829 100644 --- a/apps/docs/content/docs/en/integrations/dropbox.mdx +++ b/apps/docs/content/docs/en/integrations/dropbox.mdx @@ -44,7 +44,6 @@ Upload a file to Dropbox | --------- | ---- | -------- | ----------- | | `path` | string | Yes | The path in Dropbox where the file should be saved \(e.g., /folder/document.pdf\) | | `file` | file | No | The file to upload \(UserFile object\) | -| `fileContent` | string | No | Legacy: base64 encoded file content | | `fileName` | string | No | Optional filename \(used if path is a folder\) | | `mode` | string | No | Write mode: add \(default\) or overwrite | | `autorename` | boolean | No | If true, rename the file if there is a conflict | diff --git a/apps/docs/content/docs/en/integrations/elasticsearch.mdx b/apps/docs/content/docs/en/integrations/elasticsearch.mdx index 17239c0896a..8bc29054b0f 100644 --- a/apps/docs/content/docs/en/integrations/elasticsearch.mdx +++ b/apps/docs/content/docs/en/integrations/elasticsearch.mdx @@ -65,7 +65,6 @@ Search documents in Elasticsearch using Query DSL. Returns matching documents wi | `took` | number | Time in milliseconds the search took | | `timed_out` | boolean | Whether the search timed out | | `hits` | object | Search results with total count and matching documents | -| `aggregations` | json | Aggregation results if any | ### Elasticsearch Index Document @@ -123,7 +122,7 @@ Retrieve a document by ID from Elasticsearch. | `_index` | string | Index name | | `_id` | string | Document ID | | `_version` | number | Document version | -| `found` | boolean | Whether the document was found | +| `found` | boolean | Always true. A missing document fails the call with a 404 error rather than returning found: false. | | `_source` | json | Document content | ### Elasticsearch Update Document @@ -181,7 +180,7 @@ Delete a document from Elasticsearch by ID. | `_index` | string | Index name | | `_id` | string | Document ID | | `_version` | number | Document version | -| `result` | string | Operation result \(deleted or not_found\) | +| `result` | string | Always "deleted". A missing document fails the call with a 404 error rather than returning "not_found". | ### Elasticsearch Bulk Operations @@ -287,7 +286,7 @@ Delete an index and all its documents. This operation is irreversible. ### Elasticsearch Get Index -Retrieve index information including settings, mappings, and aliases. +Retrieve index information including settings, mappings, and aliases. Accepts a comma-separated list of indices, data streams, and aliases, and supports wildcards. #### Input @@ -300,13 +299,18 @@ Retrieve index information including settings, mappings, and aliases. | `apiKey` | string | No | Elasticsearch API key | | `username` | string | No | Username for basic auth | | `password` | string | No | Password for basic auth | -| `index` | string | Yes | Index name to retrieve info for \(e.g., "products", "logs-2024"\) | +| `index` | string | Yes | Index, data stream, or alias to retrieve info for. Accepts a comma-separated list and wildcards \(e.g., "products", "logs-2024", "logs-*", "a,b"\). | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `index` | json | Index information including aliases, mappings, and settings | +| `index` | string | Resolved index name the information belongs to | +| `aliases` | json | Aliases defined on the index | +| `mappings` | json | Field mappings for the index | +| `settings` | json | Index settings | +| `indices` | json | Every matched index keyed by its resolved name, each with its aliases, mappings, settings, and any data_stream or lifecycle. Populated for single- and multi-target requests alike. | +| `matchedCount` | number | How many indices, data streams, or aliases the target matched | ### Elasticsearch Cluster Health @@ -324,7 +328,7 @@ Get the health status of the Elasticsearch cluster. | `username` | string | No | Username for basic auth | | `password` | string | No | Password for basic auth | | `waitForStatus` | string | No | Wait until cluster reaches this status: green, yellow, or red | -| `timeout` | string | No | Timeout for the wait operation \(e.g., 30s, 1m\) | +| `esTimeout` | string | No | Elasticsearch wait timeout as a duration string \(e.g., 30s, 1m\). A bare number is read as seconds. Named esTimeout because the executor reserves "timeout" for the transport deadline in milliseconds. | #### Output diff --git a/apps/docs/content/docs/en/integrations/firecrawl.mdx b/apps/docs/content/docs/en/integrations/firecrawl.mdx index de218a6cfd7..1cdaa87f59d 100644 --- a/apps/docs/content/docs/en/integrations/firecrawl.mdx +++ b/apps/docs/content/docs/en/integrations/firecrawl.mdx @@ -52,7 +52,7 @@ Extract structured content from web pages with comprehensive metadata support. C | --------- | ---- | -------- | ----------- | | `url` | string | Yes | The URL to scrape content from \(e.g., "https://example.com/page"\) | | `formats` | json | No | Output formats supplied by existing Firecrawl block configurations | -| `scrapeOptions` | json | No | Options for content scraping | +| `firecrawlTimeout` | number | No | How long Firecrawl may spend on the scrape, in milliseconds. Sent as `timeout` in the request body; it does not bound Sim's own transport deadline. | | `apiKey` | string | Yes | Firecrawl API key | #### Output @@ -90,7 +90,6 @@ Scrape multiple URLs in a single batch job and retrieve structured content from | `onlyMainContent` | boolean | No | Extract only main content from pages | | `maxConcurrency` | number | No | Maximum number of concurrent scrapes | | `ignoreInvalidURLs` | boolean | No | Skip invalid URLs instead of failing the batch \(default: true\) | -| `scrapeOptions` | json | No | Advanced scraping configuration options | | `zeroDataRetention` | boolean | No | Enable zero data retention | | `apiKey` | string | Yes | Firecrawl API key | @@ -159,28 +158,56 @@ Search for information on the web using Firecrawl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `query` | string | Yes | The search query to use | -| `scrapeOptions` | json | No | Advanced scrape options supplied by existing configurations | +| `limit` | number | No | Maximum number of results to return per source type, not in total \(Firecrawl default: 10, maximum: 100\). Requesting three sources at limit 100 can return up to 300 results. | +| `sources` | array | No | Result sources to search: "web", "news", and/or "images". Defaults to \["web"\]. Each requested source is returned as its own array under `data` — `data.web`, `data.news`, `data.images` — with its own item fields. | +| `categories` | array | No | Restrict web results to these categories: "github", "research", "pdf", or "developer". "developer" cannot be combined with any other category. | +| `tbs` | string | No | Time-based search filter \(e.g. "qdr:d" for the past day, "qdr:w" for the past week\) | +| `location` | string | No | Location to search from \(e.g. "Germany", "San Francisco, California"\) | +| `country` | string | No | ISO country code for geo-targeting results \(e.g. "US", "DE", "JP"\). Firecrawl\'s published OpenAPI declares a bare `default: US`, but its server-side request transform applies that default only when `location` is also unset \(`x.country !== undefined ? x.country : x.location ? undefined : "us"` in the v2 search request schema\). The block ships both a Location and a Country field, so the common configuration — Location filled, Country blank — sends no country at all. Set both for best results. | +| `firecrawlTimeout` | number | No | How long Firecrawl may spend on the search, in milliseconds \(Firecrawl default: 60000\). Sent as `timeout` in the request body; it does not bound Sim's own transport deadline. | +| `ignoreInvalidURLs` | boolean | No | Skip results whose URLs cannot be scraped instead of failing the search | | `apiKey` | string | Yes | Firecrawl API key | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | array | Search results data with scraped content and metadata | -| ↳ `title` | string | Search result title from search engine | -| ↳ `description` | string | Search result description/snippet from search engine | -| ↳ `url` | string | URL of the search result | -| ↳ `markdown` | string | Page content in markdown \(when scrapeOptions.formats includes "markdown"\) | -| ↳ `html` | string | Processed HTML content \(when scrapeOptions.formats includes "html"\) | -| ↳ `rawHtml` | string | Unprocessed raw HTML \(when scrapeOptions.formats includes "rawHtml"\) | -| ↳ `links` | array | Links found on the page \(when scrapeOptions.formats includes "links"\) | -| ↳ `screenshot` | string | Screenshot URL \(expires after 24 hours, when scrapeOptions.formats includes "screenshot"\) | -| ↳ `metadata` | object | Metadata about the search result page | -| ↳ `title` | string | Page title | -| ↳ `description` | string | Page meta description | -| ↳ `sourceURL` | string | Original source URL | -| ↳ `statusCode` | number | HTTP status code | -| ↳ `error` | string | Error message if scrape failed | +| `data` | object | Search results, grouped by the sources requested | +| ↳ `web` | array | Web results \(returned by default, or when "web" is in sources\) | +| ↳ `markdown` | string | Page content in markdown \(when scrapeOptions.formats includes "markdown"\) | +| ↳ `html` | string | Processed HTML content \(when scrapeOptions.formats includes "html"\) | +| ↳ `rawHtml` | string | Unprocessed raw HTML \(when scrapeOptions.formats includes "rawHtml"\) | +| ↳ `links` | array | Links found on the page \(when scrapeOptions.formats includes "links"\) | +| ↳ `screenshot` | string | Screenshot URL \(expires after 24 hours, when scrapeOptions.formats includes "screenshot"\) | +| ↳ `audio` | string | Signed URL to the extracted MP3 \(when scrapeOptions.formats includes "audio"\); expires after 1 hour | +| ↳ `video` | string | Signed URL to the extracted video \(when scrapeOptions.formats includes "video"\); expires after 1 hour | +| ↳ `title` | string | Title from the search result | +| ↳ `description` | string | Description from the search result | +| ↳ `url` | string | URL of the search result | +| ↳ `news` | array | News results \(only when "news" is in sources\) | +| ↳ `markdown` | string | Page content in markdown \(when scrapeOptions.formats includes "markdown"\) | +| ↳ `html` | string | Processed HTML content \(when scrapeOptions.formats includes "html"\) | +| ↳ `rawHtml` | string | Unprocessed raw HTML \(when scrapeOptions.formats includes "rawHtml"\) | +| ↳ `links` | array | Links found on the page \(when scrapeOptions.formats includes "links"\) | +| ↳ `screenshot` | string | Screenshot URL \(expires after 24 hours, when scrapeOptions.formats includes "screenshot"\) | +| ↳ `audio` | string | Signed URL to the extracted MP3 \(when scrapeOptions.formats includes "audio"\); expires after 1 hour | +| ↳ `video` | string | Signed URL to the extracted video \(when scrapeOptions.formats includes "video"\); expires after 1 hour | +| ↳ `title` | string | Title of the article | +| ↳ `snippet` | string | Snippet from the article | +| ↳ `url` | string | URL of the article | +| ↳ `date` | string | Date of the article | +| ↳ `imageUrl` | string | URL of the article's image | +| ↳ `position` | number | Rank of the article within the news results \(1-based\) | +| ↳ `images` | array | Image results \(only when "images" is in sources\) | +| ↳ `title` | string | Title from the search result | +| ↳ `imageUrl` | string | URL of the image itself | +| ↳ `imageWidth` | number | Width of the image in pixels | +| ↳ `imageHeight` | number | Height of the image in pixels | +| ↳ `url` | string | URL of the page containing the image | +| ↳ `position` | number | Rank of the result within the image results \(1-based\) | +| `warning` | string | Warning message if any issues occurred during the search | +| `id` | string | ID of the search job | +| `creditsUsed` | number | Number of credits the search consumed | ### Firecrawl Crawl @@ -195,7 +222,6 @@ Crawl entire websites and extract structured content from all accessible pages | `maxDepth` | number | No | Maximum depth to crawl from the starting URL \(e.g., 1, 2, 3\). Controls how many levels deep to follow links | | `formats` | json | No | Output formats for scraped content \(e.g., \["markdown"\], \["markdown", "html"\], \["markdown", "links"\]\) | | `prompt` | string | No | Natural-language crawl guidance supplied by existing configurations | -| `scrapeOptions` | json | No | Advanced scrape options supplied by existing configurations | | `excludePaths` | json | No | URL paths to exclude from crawling \(e.g., \["/blog/*", "/admin/*", "/*.pdf"\]\) | | `includePaths` | json | No | URL paths to include in crawling \(e.g., \["/docs/*", "/api/*"\]\). Only these paths will be crawled | | `onlyMainContent` | boolean | No | Extract only main content from pages | @@ -286,7 +312,7 @@ Get a complete list of URLs from any website quickly and reliably. Useful for di | `includeSubdomains` | boolean | No | Whether to include URLs from subdomains \(default: true\) | | `ignoreQueryParameters` | boolean | No | Exclude URLs containing query strings \(default: true\) | | `limit` | number | No | Maximum number of links to return \(e.g., 100, 1000, 5000\). Max: 100,000, default: 5,000 | -| `timeout` | number | No | Request timeout in milliseconds | +| `firecrawlTimeout` | number | No | How long Firecrawl may spend on the map, in milliseconds. Sent as `timeout` in the request body; it does not bound Sim's own transport deadline. | | `location` | json | No | Geographic context for proxying \(country, languages\) | | `apiKey` | string | Yes | Firecrawl API key | @@ -295,7 +321,10 @@ Get a complete list of URLs from any website quickly and reliably. Useful for di | Parameter | Type | Description | | --------- | ---- | ----------- | | `success` | boolean | Whether the mapping operation was successful | -| `links` | array | Array of discovered URLs from the website | +| `links` | array | Discovered links. Each entry is an object with `url` and optional `title`/`description` — read `<firecrawl.links>\[i\].url` for the address, not the entry itself. | +| ↳ `url` | string | The discovered URL | +| ↳ `title` | string | Page title, when Firecrawl resolved one | +| ↳ `description` | string | Page description, when Firecrawl resolved one | ### Firecrawl Extract @@ -313,7 +342,6 @@ Extract structured data from entire webpages using natural language prompts and | `includeSubdomains` | boolean | No | Extend scanning to subdomains \(default: true\) | | `showSources` | boolean | No | Return data sources in the response \(default: false\) | | `ignoreInvalidURLs` | boolean | No | Skip invalid URLs in the array \(default: true\) | -| `scrapeOptions` | json | No | Advanced scraping configuration options | | `apiKey` | string | Yes | Firecrawl API key | #### Output diff --git a/apps/docs/content/docs/en/integrations/github.mdx b/apps/docs/content/docs/en/integrations/github.mdx index 2997205fc26..120ad61de2f 100644 --- a/apps/docs/content/docs/en/integrations/github.mdx +++ b/apps/docs/content/docs/en/integrations/github.mdx @@ -120,11 +120,8 @@ Create comments on GitHub PRs | `body` | string | Yes | Comment content | | `pullNumber` | number | Yes | Pull request number | | `path` | string | No | File path for review comment | -| `position` | number | No | Line number for review comment | | `commentType` | string | No | Type of comment \(pr_comment or file_comment\) | | `line` | number | No | Line number for review comment | -| `side` | string | No | Side of the diff \(LEFT or RIGHT\) | -| `commitId` | string | No | The SHA of the commit to comment on | | `apiKey` | string | Yes | GitHub API token | #### Output diff --git a/apps/docs/content/docs/en/integrations/google_calendar.mdx b/apps/docs/content/docs/en/integrations/google_calendar.mdx index 76216710dbd..43fc5aa5b90 100644 --- a/apps/docs/content/docs/en/integrations/google_calendar.mdx +++ b/apps/docs/content/docs/en/integrations/google_calendar.mdx @@ -89,7 +89,6 @@ List events from Google Calendar. Returns API-aligned fields only. | `maxResults` | number | No | Maximum number of events to return \(max 2500\) | | `pageToken` | string | No | Token for retrieving the next page of results | | `orderBy` | string | No | Order of events: startTime \(chronological, the default\) or updated \(last-modified\). startTime is always valid here because singleEvents is set. | -| `showDeleted` | boolean | No | Include deleted events | #### Output @@ -227,7 +226,6 @@ Get instances of a recurring event from Google Calendar. Returns API-aligned fie | `timeMax` | string | No | Upper bound for instances \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) | | `maxResults` | number | No | Maximum number of instances to return \(default 250, max 2500\) | | `pageToken` | string | No | Token for retrieving subsequent pages of results | -| `showDeleted` | boolean | No | Include deleted instances | #### Output @@ -248,8 +246,6 @@ List all calendars in the user's calendar list. Returns API-aligned fields only. | `minAccessRole` | string | No | Minimum access role for returned calendars: freeBusyReader, reader, writer, or owner | | `maxResults` | number | No | Maximum number of calendars to return \(default 100, max 250\) | | `pageToken` | string | No | Token for retrieving subsequent pages of results | -| `showDeleted` | boolean | No | Include deleted calendars | -| `showHidden` | boolean | No | Include hidden calendars | #### Output diff --git a/apps/docs/content/docs/en/integrations/google_drive.mdx b/apps/docs/content/docs/en/integrations/google_drive.mdx index d6f781a2852..e2cc4dbfec0 100644 --- a/apps/docs/content/docs/en/integrations/google_drive.mdx +++ b/apps/docs/content/docs/en/integrations/google_drive.mdx @@ -43,10 +43,8 @@ List files and folders in Google Drive with complete metadata | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `folderSelector` | string | No | Google Drive folder ID to list files from \(e.g., 1ABCxyz...\) | -| `folderId` | string | No | The ID of the folder to list files from \(internal use\) | | `query` | string | No | Search term to filter files by name \(e.g. "budget" finds files with "budget" in the name\). Do NOT use Google Drive query syntax here - just provide a plain search term. | | `pageSize` | number | No | The maximum number of files to return \(default: 100\) | -| `pageToken` | string | No | The page token to use for pagination | #### Output @@ -228,7 +226,6 @@ Create a new folder in Google Drive with complete metadata returned | --------- | ---- | -------- | ----------- | | `fileName` | string | Yes | Name of the folder to create | | `folderSelector` | string | No | Google Drive parent folder ID to create the folder in \(e.g., 1ABCxyz...\) | -| `folderId` | string | No | ID of the parent folder \(internal use\) | #### Output @@ -286,7 +283,6 @@ Upload a file to Google Drive with complete metadata returned | `content` | string | No | Text content to upload \(use this OR file, not both\) | | `mimeType` | string | No | The MIME type of the file to upload \(auto-detected from file if not provided\) | | `folderSelector` | string | No | Google Drive folder ID to upload the file to \(e.g., 1ABCxyz...\) | -| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) | #### Output @@ -485,7 +481,6 @@ Search for files in Google Drive using advanced query syntax (e.g., fullText con | --------- | ---- | -------- | ----------- | | `query` | string | Yes | Google Drive query string using advanced search syntax \(e.g., "fullText contains \'budget\'", "mimeType = \'application/pdf\'", "modifiedTime > \'2024-01-01\'"\) | | `pageSize` | number | No | Maximum number of files to return \(default: 100\) | -| `pageToken` | string | No | Token for fetching the next page of results | #### Output @@ -673,7 +668,6 @@ List all permissions (who has access) for a file in Google Drive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `fileId` | string | Yes | The ID of the file to list permissions for | -| `pageToken` | string | No | The page token to use for pagination | #### Output @@ -723,7 +717,6 @@ List the revision history of a file in Google Drive | --------- | ---- | -------- | ----------- | | `fileId` | string | Yes | The ID of the file to list revisions for | | `pageSize` | number | No | Maximum number of revisions to return \(1-1000, default 200\) | -| `pageToken` | string | No | The page token to use for pagination | #### Output @@ -783,7 +776,6 @@ List comments on a file in Google Drive | `includeDeleted` | boolean | No | Whether to include deleted comments \(their content is stripped\) | | `pageSize` | number | No | Maximum number of comments to return \(1-100, default 20\) | | `startModifiedTime` | string | No | Only return comments modified after this RFC 3339 timestamp | -| `pageToken` | string | No | The page token to use for pagination | #### Output diff --git a/apps/docs/content/docs/en/integrations/google_sheets.mdx b/apps/docs/content/docs/en/integrations/google_sheets.mdx index 70307178362..b6602c62a3e 100644 --- a/apps/docs/content/docs/en/integrations/google_sheets.mdx +++ b/apps/docs/content/docs/en/integrations/google_sheets.mdx @@ -75,7 +75,6 @@ Write data to a specific sheet in a Google Sheets spreadsheet | `cellRange` | string | No | The cell range to write to \(e.g. "A1:D10", "A1"\). Defaults to "A1" if not specified. | | `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to write | -| `includeValuesInResponse` | boolean | No | Whether to include the written values in the response | #### Output @@ -102,7 +101,6 @@ Update data in a specific sheet in a Google Sheets spreadsheet | `cellRange` | string | No | The cell range to update \(e.g. "A1:D10", "A1"\). Defaults to "A1" if not specified. | | `values` | array | Yes | The data to update as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to update | -| `includeValuesInResponse` | boolean | No | Whether to include the updated values in the response | #### Output @@ -129,7 +127,6 @@ Append data to the end of a specific sheet in a Google Sheets spreadsheet | `values` | array | Yes | The data to append as a 2D array \(e.g. \[\["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to append | | `insertDataOption` | string | No | How to insert the data \(OVERWRITE or INSERT_ROWS\) | -| `includeValuesInResponse` | boolean | No | Whether to include the appended values in the response | #### Output diff --git a/apps/docs/content/docs/en/integrations/google_vault.mdx b/apps/docs/content/docs/en/integrations/google_vault.mdx index 13579318448..5f6ed5be1f8 100644 --- a/apps/docs/content/docs/en/integrations/google_vault.mdx +++ b/apps/docs/content/docs/en/integrations/google_vault.mdx @@ -328,7 +328,7 @@ Reopen a closed matter ### Vault Delete Matter -Permanently delete a matter (must be closed first) +Soft-delete a matter (must be closed first). The matter moves to the DELETED state and can be restored with Undelete Matter #### Input @@ -360,15 +360,15 @@ Restore a deleted matter ### Vault Add Matter Collaborator -Add a collaborator (or transfer ownership) to a matter +Add a collaborator to a matter #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | -| `accountId` | string | Yes | Admin SDK account ID of the user to add as a collaborator/owner | -| `role` | string | Yes | Permission level to grant: COLLABORATOR or OWNER | +| `accountId` | string | Yes | Admin SDK account ID of the user to add as a collaborator | +| `role` | string | Yes | Permission level to grant. The API accepts COLLABORATOR only | | `sendEmails` | boolean | No | Send a notification email to the added account | | `ccMe` | boolean | No | CC the requestor on the notification email \(only relevant if sendEmails is true\) | diff --git a/apps/docs/content/docs/en/integrations/incidentio.mdx b/apps/docs/content/docs/en/integrations/incidentio.mdx index ce3d999f927..fa39c1495f5 100644 --- a/apps/docs/content/docs/en/integrations/incidentio.mdx +++ b/apps/docs/content/docs/en/integrations/incidentio.mdx @@ -61,8 +61,7 @@ List incidents from incident.io. Returns a list of incidents with their details | `incidents` | array | List of incidents | | ↳ `id` | string | Incident ID | | ↳ `name` | string | Incident name/title | -| ↳ `summary` | string | Incident summary | -| ↳ `description` | string | Incident description | +| ↳ `summary` | string | Detailed description of the incident | | ↳ `mode` | string | Incident mode \(standard, retrospective, test\) | | ↳ `call_url` | string | Video call URL | | ↳ `severity` | object | Incident severity | @@ -82,7 +81,8 @@ List incidents from incident.io. Returns a list of incidents with their details | ↳ `is_default` | boolean | Whether this is the default incident type | | ↳ `created_at` | string | When the incident was created \(ISO 8601\) | | ↳ `updated_at` | string | When the incident was last updated \(ISO 8601\) | -| ↳ `incident_url` | string | URL to the incident page | +| ↳ `permalink` | string | Permanent link to the incident | +| ↳ `incident_url` | string | Deprecated alias for permalink; kept for existing workflow references | | ↳ `slack_channel_id` | string | Slack channel ID | | ↳ `slack_channel_name` | string | Slack channel name | | ↳ `visibility` | string | Incident visibility \(public, private\) | @@ -115,8 +115,7 @@ Create a new incident in incident.io. Requires idempotency_key, severity_id, and | `incident` | object | The created incident object | | ↳ `id` | string | Incident ID | | ↳ `name` | string | Incident name | -| ↳ `summary` | string | Brief summary of the incident | -| ↳ `description` | string | Detailed description of the incident | +| ↳ `summary` | string | Detailed description of the incident | | ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) | | ↳ `call_url` | string | URL for the incident call/bridge | | ↳ `severity` | object | Severity of the incident | @@ -132,7 +131,8 @@ Create a new incident in incident.io. Requires idempotency_key, severity_id, and | ↳ `name` | string | Type name | | ↳ `created_at` | string | Creation timestamp | | ↳ `updated_at` | string | Last update timestamp | -| ↳ `incident_url` | string | URL to the incident | +| ↳ `permalink` | string | Permanent link to the incident | +| ↳ `incident_url` | string | Deprecated alias for permalink; kept for existing workflow references | | ↳ `slack_channel_id` | string | Associated Slack channel ID | | ↳ `slack_channel_name` | string | Associated Slack channel name | | ↳ `visibility` | string | Incident visibility | @@ -155,8 +155,7 @@ Retrieve detailed information about a specific incident from incident.io by its | `incident` | object | Detailed incident information | | ↳ `id` | string | Incident ID | | ↳ `name` | string | Incident name | -| ↳ `summary` | string | Brief summary of the incident | -| ↳ `description` | string | Detailed description of the incident | +| ↳ `summary` | string | Detailed description of the incident | | ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) | | ↳ `call_url` | string | URL for the incident call/bridge | | ↳ `permalink` | string | Permanent link to the incident | @@ -173,7 +172,7 @@ Retrieve detailed information about a specific incident from incident.io by its | ↳ `name` | string | Type name | | ↳ `created_at` | string | Creation timestamp | | ↳ `updated_at` | string | Last update timestamp | -| ↳ `incident_url` | string | URL to the incident | +| ↳ `incident_url` | string | Deprecated alias for permalink; kept for existing workflow references | | ↳ `slack_channel_id` | string | Associated Slack channel ID | | ↳ `slack_channel_name` | string | Associated Slack channel name | | ↳ `visibility` | string | Incident visibility | @@ -204,8 +203,7 @@ Update an existing incident in incident.io. Can update name, summary, severity, | `incident` | object | The updated incident object | | ↳ `id` | string | Incident ID | | ↳ `name` | string | Incident name | -| ↳ `summary` | string | Brief summary of the incident | -| ↳ `description` | string | Detailed description of the incident | +| ↳ `summary` | string | Detailed description of the incident | | ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) | | ↳ `call_url` | string | URL for the incident call/bridge | | ↳ `severity` | object | Severity of the incident | @@ -221,7 +219,8 @@ Update an existing incident in incident.io. Can update name, summary, severity, | ↳ `name` | string | Type name | | ↳ `created_at` | string | Creation timestamp | | ↳ `updated_at` | string | Last update timestamp | -| ↳ `incident_url` | string | URL to the incident | +| ↳ `permalink` | string | Permanent link to the incident | +| ↳ `incident_url` | string | Deprecated alias for permalink; kept for existing workflow references | | ↳ `slack_channel_id` | string | Associated Slack channel ID | | ↳ `slack_channel_name` | string | Associated Slack channel name | | ↳ `visibility` | string | Incident visibility | @@ -250,7 +249,6 @@ List actions from incident.io. Optionally filter by incident ID. | ↳ `name` | string | User name | | ↳ `email` | string | User email | | ↳ `status` | string | Action status | -| ↳ `due_at` | string | Due date/time | | ↳ `created_at` | string | Creation timestamp | | ↳ `updated_at` | string | Last update timestamp | | ↳ `incident_id` | string | Associated incident ID | @@ -259,10 +257,6 @@ List actions from incident.io. Optionally filter by incident ID. | ↳ `name` | string | User name | | ↳ `email` | string | User email | | ↳ `completed_at` | string | Completion timestamp | -| ↳ `external_issue_reference` | object | External issue tracking reference | -| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) | -| ↳ `issue_name` | string | Issue identifier | -| ↳ `issue_permalink` | string | URL to the external issue | ### incident.io Actions Show @@ -287,7 +281,6 @@ Get detailed information about a specific action from incident.io. | ↳ `name` | string | User name | | ↳ `email` | string | User email | | ↳ `status` | string | Action status | -| ↳ `due_at` | string | Due date/time | | ↳ `created_at` | string | Creation timestamp | | ↳ `updated_at` | string | Last update timestamp | | ↳ `incident_id` | string | Associated incident ID | @@ -296,10 +289,6 @@ Get detailed information about a specific action from incident.io. | ↳ `name` | string | User name | | ↳ `email` | string | User email | | ↳ `completed_at` | string | Completion timestamp | -| ↳ `external_issue_reference` | object | External issue tracking reference | -| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) | -| ↳ `issue_name` | string | Issue identifier | -| ↳ `issue_permalink` | string | URL to the external issue | ### incident.io Follow-ups List @@ -414,7 +403,6 @@ List all users in your Incident.io workspace. Returns user details including id, | `pagination_meta` | object | Pagination metadata | | ↳ `after` | string | Cursor for next page | | ↳ `page_size` | number | Number of items per page | -| ↳ `total_record_count` | number | Total number of records | ### Incident.io Users Show @@ -486,7 +474,7 @@ Create a new workflow in incident.io. | `trigger` | string | No | Trigger type for the workflow \(e.g., "incident.updated", "incident.created"\) | | `steps` | string | No | Array of workflow steps as JSON string. Example: \[\{"label": "Notify team", "name": "slack.post_message"\}\] | | `condition_groups` | string | No | Array of condition groups as JSON string to control when the workflow runs. Example: \[\{"conditions": \[\{"operation": "one_of", "param_bindings": \[\], "subject": "incident.severity"\}\]\}\] | -| `runs_on_incidents` | string | No | When to run the workflow: "newly_created" \(only new incidents\), "newly_created_and_active" \(new and active incidents\), "active" \(only active incidents\), or "all" \(all incidents\) | +| `runs_on_incidents` | string | No | When to run the workflow: "newly_created" \(only incidents created after the workflow is enabled\) or "newly_created_and_active" \(also applies to incidents that are already open\) | | `runs_on_incident_modes` | string | No | Array of incident modes to run on as JSON string. Example: \["standard", "retrospective"\] | | `include_private_incidents` | boolean | No | Whether to include private incidents | | `continue_on_step_error` | boolean | No | Whether to continue executing subsequent steps if a step fails | @@ -569,7 +557,7 @@ Update an existing workflow in incident.io. | `name` | string | Yes | New name for the workflow \(e.g., "Notify on Critical Incidents"\) | | `steps` | string | Yes | Complete array of workflow steps as a JSON string | | `condition_groups` | string | Yes | Complete array of workflow condition groups as a JSON string | -| `runs_on_incidents` | string | Yes | When to run the workflow: newly_created, newly_created_and_active, active, or all | +| `runs_on_incidents` | string | Yes | When to run the workflow: newly_created or newly_created_and_active | | `runs_on_incident_modes` | string | Yes | Complete array of incident modes to run on as a JSON string | | `include_private_incidents` | boolean | Yes | Whether to include private incidents | | `continue_on_step_error` | boolean | Yes | Whether to continue executing subsequent steps if a step fails | @@ -1760,7 +1748,6 @@ List the entries of a catalog type in incident.io — for example every service, | `pagination_meta` | object | Pagination metadata | | ↳ `after` | string | Cursor for next page | | ↳ `page_size` | number | Number of results per page | -| ↳ `total_record_count` | number | Total number of entries | ### List Teams diff --git a/apps/docs/content/docs/en/integrations/jira.mdx b/apps/docs/content/docs/en/integrations/jira.mdx index ffa2803f524..28fe73abd5c 100644 --- a/apps/docs/content/docs/en/integrations/jira.mdx +++ b/apps/docs/content/docs/en/integrations/jira.mdx @@ -49,7 +49,6 @@ Retrieve detailed information about a specific Jira issue | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key to retrieve \(e.g., PROJ-123\) | | `includeAttachments` | boolean | No | Download attachment file contents and include them as files in the output | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -262,7 +261,6 @@ Update a Jira issue | `customFieldId` | string | No | Custom field ID to update \(e.g., customfield_10001\) | | `customFieldValue` | string | No | Value for the custom field | | `notifyUsers` | boolean | No | Whether to send email notifications about this update \(default: true\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -287,7 +285,6 @@ Create a new Jira issue | `description` | string | No | Description for the issue. Accepts plain text \(auto-wrapped in ADF\) or a raw ADF document object | | `priority` | string | No | Priority ID or name for the issue \(e.g., "10000" or "High"\) | | `assignee` | string | No | Assignee account ID for the issue | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | | `issueType` | string | Yes | Type of issue to create \(e.g., Task, Story, Bug, Epic, Sub-task\) | | `parent` | json | No | Parent issue key for creating subtasks \(e.g., \{ "key": "PROJ-123" \}\) | | `labels` | array | No | Labels for the issue \(array of label names\) | @@ -322,7 +319,6 @@ Retrieve multiple Jira issues from a project in bulk | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `projectId` | string | Yes | Jira project key \(e.g., PROJ\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -364,7 +360,6 @@ Delete a Jira issue | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key to delete \(e.g., PROJ-123\) | | `deleteSubtasks` | boolean | No | Whether to delete subtasks. If false, parent issues with subtasks cannot be deleted. | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -385,7 +380,6 @@ Assign a Jira issue to a user | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key to assign \(e.g., PROJ-123\) | | `accountId` | string | Yes | Account ID of the user to assign the issue to. Use "-1" for automatic assignment, or leave empty / pass "null" to unassign. | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -409,7 +403,6 @@ Move a Jira issue between workflow statuses (e.g., To Do -> In Progress) | `transitionId` | string | Yes | ID of the transition to execute \(e.g., "11" for "To Do", "21" for "In Progress"\) | | `comment` | string | No | Optional comment to add when transitioning the issue | | `resolution` | string | No | Resolution name to set during transition \(e.g., "Fixed", "Won\'t Fix"\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -437,7 +430,6 @@ Search for Jira issues using JQL (Jira Query Language) | `nextPageToken` | string | No | Cursor token for the next page of results. Omit for the first page. | | `maxResults` | number | No | Maximum number of results to return per page \(default: 50\) | | `fields` | array | No | Array of field names to return \(default: all fields\). | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -520,7 +512,6 @@ Add a comment to a Jira issue | `issueKey` | string | Yes | Jira issue key to add comment to \(e.g., PROJ-123\) | | `body` | string | Yes | Comment body text | | `visibility` | json | No | Restrict comment visibility. Object with "type" \("role" or "group"\) and "value" \(role/group name\). | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -554,8 +545,7 @@ Get all comments from a Jira issue | `issueKey` | string | Yes | Jira issue key to get comments from \(e.g., PROJ-123\) | | `startAt` | number | No | Index of the first comment to return \(default: 0\) | | `maxResults` | number | No | Maximum number of comments to return \(default: 50\) | -| `orderBy` | string | No | Sort order for comments: "-created" for newest first, "created" for oldest first | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | +| `orderBy` | string | No | Sort order for comments. Must be exactly "created", "-created" \(newest first\), or "+created"; any other value is rejected. | #### Output @@ -605,7 +595,6 @@ Update an existing comment on a Jira issue | `commentId` | string | Yes | ID of the comment to update | | `body` | string | Yes | Updated comment text | | `visibility` | json | No | Restrict comment visibility. Object with "type" \("role" or "group"\) and "value" \(role/group name\). | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -638,7 +627,6 @@ Delete a comment from a Jira issue | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key containing the comment \(e.g., PROJ-123\) | | `commentId` | string | Yes | ID of the comment to delete | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -660,7 +648,6 @@ Get all attachments from a Jira issue | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key to get attachments from \(e.g., PROJ-123\) | | `includeAttachments` | boolean | No | Download attachment file contents and include them as files in the output | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -698,7 +685,6 @@ Add attachments to a Jira issue | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key to add attachments to \(e.g., PROJ-123\) | | `files` | file[] | Yes | Files to attach to the Jira issue | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -725,7 +711,6 @@ Delete an attachment from a Jira issue | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `attachmentId` | string | Yes | ID of the attachment to delete | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -749,7 +734,6 @@ Add a time tracking worklog entry to a Jira issue | `comment` | string | No | Optional comment for the worklog entry | | `started` | string | No | Optional start time in ISO format \(defaults to current time\) | | `visibility` | json | No | Restrict worklog visibility. Object with "type" \("role" or "group"\) and "value" \(role/group name\). | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -784,7 +768,6 @@ Get all worklog entries from a Jira issue | `issueKey` | string | Yes | Jira issue key to get worklogs from \(e.g., PROJ-123\) | | `startAt` | number | No | Index of the first worklog to return \(default: 0\) | | `maxResults` | number | No | Maximum number of worklogs to return \(default: 50\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -836,7 +819,6 @@ Update an existing worklog entry on a Jira issue | `comment` | string | No | Optional comment for the worklog entry | | `started` | string | No | Optional start time in ISO format | | `visibility` | json | No | Restrict worklog visibility. Object with "type" \("role" or "group"\) and "value" \(role/group name\). | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -880,7 +862,6 @@ Delete a worklog entry from a Jira issue | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key containing the worklog \(e.g., PROJ-123\) | | `worklogId` | string | Yes | ID of the worklog entry to delete | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -904,7 +885,6 @@ Create a link relationship between two Jira issues | `outwardIssueKey` | string | Yes | Jira issue key for the outward issue \(e.g., PROJ-456\) | | `linkType` | string | Yes | The type of link relationship \(e.g., "Blocks", "Relates to", "Duplicates"\) | | `comment` | string | No | Optional comment to add to the issue link | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -927,7 +907,6 @@ Delete a link between two Jira issues | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `linkId` | string | Yes | ID of the issue link to delete | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -948,7 +927,6 @@ Add a watcher to a Jira issue to receive notifications about updates | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key to add watcher to \(e.g., PROJ-123\) | | `accountId` | string | Yes | Account ID of the user to add as watcher | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -970,7 +948,6 @@ Remove a watcher from a Jira issue | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | Jira issue key to remove watcher from \(e.g., PROJ-123\) | | `accountId` | string | Yes | Account ID of the user to remove as watcher | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -993,7 +970,6 @@ Get Jira users. If an account ID is provided, returns a single user. Otherwise, | `accountId` | string | No | Optional account ID to get a specific user. If not provided, returns all users. | | `startAt` | number | No | The index of the first user to return \(for pagination, default: 0\) | | `maxResults` | number | No | Maximum number of users to return \(default: 50\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -1026,7 +1002,6 @@ Search for Jira users by email address or display name. Returns matching users w | `query` | string | Yes | A query string to search for users. Can be an email address, display name, or partial match. | | `maxResults` | number | No | Maximum number of users to return \(default: 50, max: 1000\) | | `startAt` | number | No | The index of the first user to return \(for pagination, default: 0\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -1058,7 +1033,6 @@ List Jira projects visible to the user, with optional name/key filtering and pag | `query` | string | No | Filter projects by partial name or key match | | `startAt` | number | No | The index of the first project to return \(for pagination, default: 0\) | | `maxResults` | number | No | Maximum number of projects to return \(default: 50, max: 100\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -1091,7 +1065,6 @@ Get the details of a single Jira project by its ID or key, including its type, l | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `projectId` | string | Yes | The project ID or key \(e.g., "PROJ" or "10000"\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -1124,7 +1097,6 @@ Get the workflow transitions available for an issue in its current status. Use t | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `issueKey` | string | Yes | The issue key or ID \(e.g., PROJ-123\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -1151,7 +1123,6 @@ List all issue types visible to the user across projects (e.g., Task, Bug, Story | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -1177,7 +1148,6 @@ Get all system and custom fields defined in the Jira instance. Useful for discov | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output diff --git a/apps/docs/content/docs/en/integrations/jira_service_management.mdx b/apps/docs/content/docs/en/integrations/jira_service_management.mdx index 3054fb35efe..54bcb12b8fb 100644 --- a/apps/docs/content/docs/en/integrations/jira_service_management.mdx +++ b/apps/docs/content/docs/en/integrations/jira_service_management.mdx @@ -45,7 +45,6 @@ Get all service desks from Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `expand` | string | No | Comma-separated fields to expand in the response | | `start` | number | No | Start index for pagination \(e.g., 0, 50, 100\) | | `limit` | number | No | Maximum results to return \(e.g., 10, 25, 50\) | @@ -75,7 +74,6 @@ Get request types for a service desk in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | | `searchQuery` | string | No | Filter request types by name | | `groupId` | string | No | Filter by request type group ID | @@ -110,7 +108,6 @@ Create a new service request in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | | `requestTypeId` | string | Yes | Request Type ID \(e.g., "10", "15"\) | | `summary` | string | No | Summary/title for the service request \(required unless using Form Answers\) | @@ -145,7 +142,6 @@ Get a single service request from Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `expand` | string | No | Comma-separated fields to expand: participant, status, sla, requestType, serviceDesk, attachment, comment, action | @@ -185,7 +181,6 @@ Get multiple service requests from Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | No | Filter by service desk ID \(e.g., "1", "2"\) | | `requestOwnership` | string | No | Filter by ownership: OWNED_REQUESTS, PARTICIPATED_REQUESTS, APPROVER, ALL_REQUESTS | | `requestStatus` | string | No | Filter by status: OPEN_REQUESTS, CLOSED_REQUESTS, ALL_REQUESTS | @@ -232,7 +227,6 @@ Add a comment (public or internal) to a service request in Jira Service Manageme | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `body` | string | Yes | Comment body text | | `isPublic` | boolean | Yes | Whether the comment is public \(visible to customer\) or internal \(true/false\) | @@ -263,7 +257,6 @@ Get comments for a service request in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `isPublic` | boolean | No | Filter to only public comments \(true/false\) | | `internal` | boolean | No | Filter to only internal comments \(true/false\) | @@ -300,7 +293,6 @@ Get customers for a service desk in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | | `query` | string | No | Search query to filter customers \(e.g., "john", "acme"\) | | `start` | number | No | Start index for pagination \(e.g., 0, 50, 100\) | @@ -329,7 +321,6 @@ Add customers to a service desk in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | | `accountIds` | string | Yes | Comma-separated Atlassian account IDs to add as customers | @@ -350,7 +341,6 @@ Get organizations for a service desk in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | | `start` | number | No | Start index for pagination \(e.g., 0, 50, 100\) | | `limit` | number | No | Maximum results to return \(e.g., 10, 25, 50\) | @@ -375,7 +365,6 @@ Create a new organization in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `name` | string | Yes | Name of the organization to create | #### Output @@ -396,7 +385,6 @@ Add an organization to a service desk in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | | `organizationId` | string | Yes | Organization ID to add to the service desk | @@ -418,7 +406,6 @@ Get queues for a service desk in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | | `includeCount` | boolean | No | Include issue count for each queue \(true/false\) | | `start` | number | No | Start index for pagination \(e.g., 0, 50, 100\) | @@ -447,7 +434,6 @@ Get SLA information for a service request in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `start` | number | No | Start index for pagination \(e.g., 0, 50, 100\) | | `limit` | number | No | Maximum results to return \(e.g., 10, 25, 50\) | @@ -475,7 +461,6 @@ Get available transitions for a service request in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `start` | number | No | Start index for pagination \(e.g., 0, 50, 100\) | | `limit` | number | No | Maximum results to return \(e.g., 10, 25, 50\) | @@ -501,7 +486,6 @@ Transition a service request to a new status in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `transitionId` | string | Yes | Transition ID to apply | | `comment` | string | No | Optional comment to add during transition | @@ -524,7 +508,6 @@ Get participants for a request in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `start` | number | No | Start index for pagination \(e.g., 0, 50, 100\) | | `limit` | number | No | Maximum results to return \(e.g., 10, 25, 50\) | @@ -552,7 +535,6 @@ Add participants to a request in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `accountIds` | string | Yes | Comma-separated account IDs to add as participants | @@ -578,7 +560,6 @@ Get approvals for a request in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `start` | number | No | Start index for pagination \(e.g., 0, 50, 100\) | | `limit` | number | No | Maximum results to return \(e.g., 10, 25, 50\) | @@ -615,7 +596,6 @@ Approve or decline an approval request in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) | | `approvalId` | string | Yes | Approval ID to answer | | `decision` | string | Yes | Decision: "approve" or "decline" | @@ -653,7 +633,6 @@ Get the fields required to create a request of a specific type in Jira Service M | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) | | `requestTypeId` | string | Yes | Request Type ID \(e.g., "10", "15"\) | @@ -686,7 +665,6 @@ List forms (ProForma/JSM Forms) in a Jira project to discover form IDs for reque | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `projectIdOrKey` | string | Yes | Jira project ID or key \(e.g., "10001" or "SD"\) | #### Output @@ -714,7 +692,6 @@ Get the full structure of a ProForma/JSM form including all questions, field typ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `projectIdOrKey` | string | Yes | Jira project ID or key \(e.g., "10001" or "SD"\) | | `formId` | string | Yes | Form ID \(UUID from Get Form Templates\) | @@ -738,7 +715,6 @@ List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, su | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123", "10001"\) | #### Output @@ -766,7 +742,6 @@ Attach a form template to an existing Jira issue or JSM request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key to attach the form to \(e.g., "SD-123"\) | | `formTemplateId` | string | Yes | Form template UUID \(from Get Form Templates\) | @@ -793,7 +768,6 @@ Save answers to a form attached to a Jira issue or JSM request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) | | `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) | | `answers` | json | Yes | Form answers using numeric question IDs as keys \(e.g., \{"1": \{"text": "Title"\}, "4": \{"choices": \["5"\]\}\}\) | @@ -817,7 +791,6 @@ Submit a form on a Jira issue or JSM request, locking it from further edits | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) | | `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) | @@ -839,7 +812,6 @@ Get a single form with full design, state, and answers from a Jira issue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) | | `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) | @@ -863,7 +835,6 @@ Get simplified answers from a form attached to a Jira issue or JSM request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) | | `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) | @@ -885,7 +856,6 @@ Reopen a submitted form on a Jira issue or JSM request, allowing further edits | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) | | `formId` | string | Yes | Form instance UUID \(from Get Issue Forms\) | @@ -907,7 +877,6 @@ Remove a form from a Jira issue or JSM request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) | | `formId` | string | Yes | Form instance UUID to delete | @@ -929,7 +898,6 @@ Make a form visible to customers on a Jira issue or JSM request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) | | `formId` | string | Yes | Form instance UUID | @@ -951,7 +919,6 @@ Make a form internal only (not visible to customers) on a Jira issue or JSM requ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) | | `formId` | string | Yes | Form instance UUID | @@ -973,7 +940,6 @@ Copy forms from one Jira issue to another | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | | `sourceIssueIdOrKey` | string | Yes | Source issue ID or key to copy forms from \(e.g., "SD-123"\) | | `targetIssueIdOrKey` | string | Yes | Target issue ID or key to copy forms to \(e.g., "SD-456"\) | | `formIds` | json | No | Optional JSON array of form UUIDs to copy \(e.g., \["uuid1", "uuid2"\]\). If omitted, copies all forms. | @@ -997,8 +963,6 @@ List Assets (Insight/CMDB) object schemas in Jira Service Management | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `startAt` | number | No | Pagination start index \(e.g., 0, 50\) | | `maxResults` | number | No | Maximum schemas to return \(e.g., 25, 50\) | | `includeCounts` | boolean | No | Include object and object-type counts per schema | @@ -1028,8 +992,6 @@ Get a single Assets (Insight/CMDB) object schema by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `schemaId` | string | Yes | The Assets object schema ID | #### Output @@ -1055,8 +1017,6 @@ List object types within an Assets (Insight/CMDB) object schema | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `schemaId` | string | Yes | The Assets object schema ID to list object types for | | `excludeAbstract` | boolean | No | Exclude abstract object types from the result | @@ -1084,8 +1044,6 @@ Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `objectTypeId` | string | Yes | The Assets object type ID | | `onlyValueEditable` | boolean | No | Return only attributes whose values can be edited | | `query` | string | No | Filter attributes by a search query | @@ -1116,8 +1074,6 @@ Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. obj | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `qlQuery` | string | Yes | AQL query string \(e.g., objectType = "Host" AND "Operating System" = "Ubuntu"\) | | `page` | number | No | Page number \(1-based, defaults to 1\) | | `resultsPerPage` | number | No | Results per page \(e.g., 25, 50\) | @@ -1149,8 +1105,6 @@ Get a single Assets (Insight/CMDB) object by ID, including its attribute values | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `objectId` | string | Yes | The Assets object ID | #### Output @@ -1179,8 +1133,6 @@ Create an Assets (Insight/CMDB) object of a given object type. Attributes use ob | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `objectTypeId` | string | Yes | The object type ID to create the object under | | `attributes` | json | Yes | Array of attributes: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] | @@ -1210,8 +1162,6 @@ Update an existing Assets (Insight/CMDB) object. Provide the attributes to chang | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `objectId` | string | Yes | The Assets object ID to update | | `attributes` | json | Yes | Array of attributes to set: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] | | `objectTypeId` | string | No | Optional object type ID \(only if changing the type\) | @@ -1242,8 +1192,6 @@ Delete an Assets (Insight/CMDB) object by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | -| `cloudId` | string | No | Jira Cloud ID for the instance | -| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | | `objectId` | string | Yes | The Assets object ID to delete | #### Output diff --git a/apps/docs/content/docs/en/integrations/jupyter.mdx b/apps/docs/content/docs/en/integrations/jupyter.mdx index ffa7b72c730..636d1af4922 100644 --- a/apps/docs/content/docs/en/integrations/jupyter.mdx +++ b/apps/docs/content/docs/en/integrations/jupyter.mdx @@ -117,7 +117,6 @@ Upload a file to a Jupyter server | `token` | string | Yes | Jupyter server authentication token | | `directory` | string | No | Destination directory, relative to the server root. Leave blank to upload to the root directory. | | `file` | file | No | The file to upload \(UserFile object\) | -| `fileContent` | string | No | Legacy: base64 encoded file content | | `fileName` | string | No | Optional filename override | #### Output diff --git a/apps/docs/content/docs/en/integrations/langsmith.mdx b/apps/docs/content/docs/en/integrations/langsmith.mdx index 8f28f774059..a728416e92a 100644 --- a/apps/docs/content/docs/en/integrations/langsmith.mdx +++ b/apps/docs/content/docs/en/integrations/langsmith.mdx @@ -109,7 +109,7 @@ This tool does not produce any outputs. ### LangSmith Get Run -Retrieve a single LangSmith run by ID. +Retrieve a single LangSmith run by ID. Uses the v1 run read, which LangSmith has deprecated and removes on 31 Jan 2027. #### Input @@ -149,9 +149,10 @@ Attach a score, correction, or comment to a LangSmith run. | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | LangSmith API key | | `runId` | string | Yes | ID of the run to attach feedback to | +| `sessionId` | string | No | UUID of the tracing project \(session\) the feedback belongs to. LangSmith documents this as required: "POST /api/v1/feedback now requires a session_id field in the request body. It was previously optional." The official SDK warns when it is omitted and raises outright on SmithDB-only deployments, because some LangSmith deployments cannot locate the run without it. Only `key` is in the endpoint\'s OpenAPI required array, so omitting session_id will not fail request validation — but always supply it when you have it. Never guess it — use the sessionId reported by a preceding langsmith_get_run for the same run, or look the project up with GET /api/v1/sessions. | | `key` | string | Yes | Feedback metric name \(e.g. "correctness", "user_score"\) | | `score` | number | No | Numeric score for the feedback metric | -| `value` | string | No | Categorical value for the feedback metric | +| `value` | string | No | Categorical value for the feedback metric, e.g. "good". A JSON scalar or object typed here \("1", "true", \'\{"label":"good"\}\'\) is sent with its JSON type; anything else is sent as a string. | | `comment` | string | No | Free-text comment explaining the feedback | | `correction` | json | No | Corrected output for the run | | `feedbackSourceType` | string | No | Origin of the feedback \(api, app, or model\) | @@ -164,7 +165,7 @@ Attach a score, correction, or comment to a LangSmith run. | `key` | string | Feedback metric name | | `runId` | string | ID of the run the feedback was attached to | | `score` | number | Score recorded for the feedback | -| `value` | string | Categorical value recorded for the feedback | +| `value` | json | Categorical value recorded for the feedback. Declared as json because the response echoes LangSmith's own union — string, number, boolean, or JSON object — even though the input is typed as a string. | | `comment` | string | Comment recorded for the feedback | | `createdAt` | string | When the feedback was created \(ISO\) | diff --git a/apps/docs/content/docs/en/integrations/linkedin.mdx b/apps/docs/content/docs/en/integrations/linkedin.mdx index 0ecee87159b..0165395d614 100644 --- a/apps/docs/content/docs/en/integrations/linkedin.mdx +++ b/apps/docs/content/docs/en/integrations/linkedin.mdx @@ -41,7 +41,7 @@ Share a post to your personal LinkedIn feed | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `text` | string | Yes | The text content of your LinkedIn post | -| `visibility` | string | No | Who can see this post: "PUBLIC" or "CONNECTIONS" \(default: "PUBLIC"\) | +| `visibility` | string | No | Who can see this post: "PUBLIC" \(anyone on LinkedIn\), "CONNECTIONS" \(1st-degree connections only\), or "LOGGED_IN" \(signed-in LinkedIn members only\). Default: "PUBLIC" | | `request` | string | No | No description | | `output` | string | No | No description | | `output` | string | No | No description | @@ -56,10 +56,8 @@ Share a post to your personal LinkedIn feed | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `postId` | string | Created post ID | -| `profile` | json | LinkedIn profile information | -| `error` | string | Error message if operation failed | +| `postId` | string | The URN of the created post, read from the `x-restli-id` response header, which LinkedIn documents as carrying the ugcPost id. Reported as received. Absent when LinkedIn omits that header. | +| `postUrl` | string | LinkedIn URL of the created post. Viewable by an authorized LinkedIn member — not a guaranteed public permalink. Absent when the `x-restli-id` header was missing or did not carry a `urn:li:<entityType>:<id>` URN. | ### Get LinkedIn Profile @@ -74,9 +72,10 @@ Retrieve your LinkedIn profile information | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `postId` | string | Created post ID | -| `profile` | json | LinkedIn profile information | -| `error` | string | Error message if operation failed | +| `profile` | object | The authenticated LinkedIn member, from the OpenID Connect userinfo endpoint | +| ↳ `id` | string | LinkedIn member id \(the OpenID `sub` claim\) | +| ↳ `name` | string | Full display name | +| ↳ `email` | string | Primary email address. Requires the `email` scope, and LinkedIn documents the claim as optional — it may be absent even when the scope was granted | +| ↳ `picture` | string | URL of the profile picture, when the member has one | diff --git a/apps/docs/content/docs/en/integrations/linq.mdx b/apps/docs/content/docs/en/integrations/linq.mdx index 5e21505f7cd..c90d57b6fed 100644 --- a/apps/docs/content/docs/en/integrations/linq.mdx +++ b/apps/docs/content/docs/en/integrations/linq.mdx @@ -108,7 +108,6 @@ Upload a file to Linq as a reusable attachment (max 100MB) and get an attachment | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Linq API key | | `file` | file | No | File to upload \(a UserFile from a file-upload field or a previous block\) | -| `fileContent` | string | No | Legacy base64-encoded file content fallback | | `filename` | string | No | Override the file name \(defaults to the uploaded file name\) | | `contentType` | string | No | Override the MIME type \(defaults to the uploaded file type\) | diff --git a/apps/docs/content/docs/en/integrations/microsoft_dataverse.mdx b/apps/docs/content/docs/en/integrations/microsoft_dataverse.mdx index 8d7f2a1ec4e..a969b9092dc 100644 --- a/apps/docs/content/docs/en/integrations/microsoft_dataverse.mdx +++ b/apps/docs/content/docs/en/integrations/microsoft_dataverse.mdx @@ -399,7 +399,6 @@ Upload a file to a file or image column on a Dataverse record. Supports single-r | `fileColumn` | string | Yes | File or image column logical name \(e.g., entityimage, cr_document\) | | `fileName` | string | Yes | Name of the file being uploaded \(e.g., document.pdf\) | | `file` | file | No | File to upload \(UserFile object\) | -| `fileContent` | string | No | Base64-encoded file content \(legacy\) | #### Output diff --git a/apps/docs/content/docs/en/integrations/microsoft_excel.mdx b/apps/docs/content/docs/en/integrations/microsoft_excel.mdx index fffb1d43366..ef12b24a054 100644 --- a/apps/docs/content/docs/en/integrations/microsoft_excel.mdx +++ b/apps/docs/content/docs/en/integrations/microsoft_excel.mdx @@ -74,7 +74,6 @@ Write data to a specific sheet in a Microsoft Excel spreadsheet | `cellRange` | string | No | The cell range to write to \(e.g., "A1:D10", "A1"\). Defaults to "A1" if not specified. | | `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to write | -| `includeValuesInResponse` | boolean | No | Whether to include the written values in the response | #### Output diff --git a/apps/docs/content/docs/en/integrations/mistral_parse.mdx b/apps/docs/content/docs/en/integrations/mistral_parse.mdx index 3f9ec32583a..a544af150d9 100644 --- a/apps/docs/content/docs/en/integrations/mistral_parse.mdx +++ b/apps/docs/content/docs/en/integrations/mistral_parse.mdx @@ -39,7 +39,6 @@ Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF do | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `file` | file | Yes | Normalized UserFile from file upload or file reference | #### Output diff --git a/apps/docs/content/docs/en/integrations/netsuite.mdx b/apps/docs/content/docs/en/integrations/netsuite.mdx index 5421a098097..18baa8e0ce4 100644 --- a/apps/docs/content/docs/en/integrations/netsuite.mdx +++ b/apps/docs/content/docs/en/integrations/netsuite.mdx @@ -56,7 +56,6 @@ List one page of a NetSuite record collection, optionally filtered with a q expr | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `q` | string | No | NetSuite record collection filter expression | | `limit` | number | No | Results to return in this page \(1-1000; default 100\) | @@ -90,7 +89,6 @@ Retrieve one NetSuite record by internal or external ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: | | `fields` | string | No | Comma-separated record fields to return | @@ -113,7 +111,6 @@ Create a NetSuite record using the account-specific record metadata schema. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `body` | json | Yes | Record fields matching the account-specific NetSuite metadata schema | | `replace` | string | No | Comma-separated sublists whose default lines should be replaced | @@ -135,7 +132,6 @@ Update fields on an existing NetSuite record with PATCH. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: | | `body` | json | Yes | Record fields matching the account-specific NetSuite metadata schema | @@ -158,7 +154,6 @@ Create or update a NetSuite record by external ID with PUT. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `externalId` | string | Yes | External ID without the eid: prefix | | `body` | json | Yes | Record fields matching the account-specific NetSuite metadata schema | @@ -180,7 +175,6 @@ Delete one NetSuite record by internal or external ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: | @@ -200,7 +194,6 @@ Retrieve a record sublist, subrecord, referenced record, or nested subresource. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: | | `subresourcePath` | string | Yes | Slash-separated subresource path, such as item or item/1/inventoryDetail | @@ -221,7 +214,6 @@ Return a prepopulated create form, or an edit form when a record ID is supplied. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | No | Existing record ID; omit to request a create form | | `body` | json | No | Record fields matching the account-specific NetSuite metadata schema | @@ -245,7 +237,6 @@ Retrieve valid select values for one or more fields on a new or existing record. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | No | Existing record ID; omit to evaluate options for a new record | | `fields` | string | Yes | Comma-separated select field IDs | @@ -273,7 +264,6 @@ Attach a contact or file to another NetSuite record. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: | | `relatedType` | string | Yes | Related resource type: contact or file | @@ -297,7 +287,6 @@ Detach a contact or file from another NetSuite record. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: | | `relatedType` | string | Yes | Related resource type: contact or file | @@ -319,7 +308,6 @@ Execute a supported NetSuite record action such as approve, reject, or confirm. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: | | `action` | string | Yes | NetSuite record action ID without the @ prefix | @@ -342,7 +330,6 @@ Transform a supported source record into another NetSuite record type. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: | | `targetRecordType` | string | Yes | Target record type supported by the source record metadata | @@ -365,7 +352,6 @@ Submit an asynchronous request to retrieve up to 100 records of one type. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `ids` | string | Yes | Up to 100 comma-separated internal IDs or eid: external-ID references | | `fields` | string | No | Comma-separated record fields to return | @@ -391,7 +377,6 @@ Submit an asynchronous batch that creates up to 100 records of one type. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `items` | array | Yes | Array of 1-100 records matching the account-specific metadata schema | | `idempotencyKey` | string | No | Optional unique idempotency key for retrying the batch | @@ -414,7 +399,6 @@ Submit an asynchronous batch that updates up to 100 records of one type. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `items` | array | Yes | Array of 1-100 records; every item must include an internal or external ID | | `idempotencyKey` | string | No | Optional unique idempotency key for retrying the batch | @@ -437,7 +421,6 @@ Submit an asynchronous batch that creates or updates up to 100 records by extern | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `items` | array | Yes | Array of 1-100 records; every item must include externalId | | `idempotencyKey` | string | No | Optional unique idempotency key for retrying the batch | @@ -460,7 +443,6 @@ Submit an asynchronous request to delete up to 100 records of one type. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `ids` | string | Yes | Up to 100 comma-separated internal IDs or eid: external-ID references | | `idempotencyKey` | string | No | Optional unique idempotency key for retrying the batch | @@ -483,7 +465,6 @@ Execute one page of a SuiteQL query through SuiteTalk REST web services. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `query` | string | Yes | SuiteQL SELECT query; use a complete unique ORDER BY when retrieving multiple pages | | `limit` | number | No | Results to return in this page \(1-1000; default 100\) | | `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages | @@ -512,7 +493,6 @@ List one page of SuiteAnalytics Workbook datasets available to the authenticated | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `limit` | number | No | Results to return in this page \(1-1000; default 100\) | | `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages | @@ -540,7 +520,6 @@ Execute one page of a standard or custom SuiteAnalytics Workbook dataset. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `datasetId` | string | Yes | SuiteAnalytics dataset script ID | | `limit` | number | No | Results to return in this page \(1-1000; default 100\) | | `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages | @@ -569,7 +548,6 @@ List record types exposed to the authenticated role by the REST metadata catalog | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | #### Output @@ -596,7 +574,6 @@ Retrieve account-specific metadata for one NetSuite record type. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder | | `format` | string | No | Metadata representation: default, openapi, or json_schema | @@ -616,7 +593,6 @@ Retrieve job status, list job tasks, or retrieve one task status. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `jobId` | string | Yes | Asynchronous job ID | | `view` | string | No | Retrieve job status, list tasks for the job, or retrieve one task status | | `taskId` | string | No | Task ID; required when view is task | @@ -654,7 +630,6 @@ Retrieve the provider response for one task within a completed asynchronous job. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | | `jobId` | string | Yes | Asynchronous job ID | | `taskId` | string | Yes | Task ID within the asynchronous job | @@ -674,7 +649,6 @@ Retrieve the current UTC time from the NetSuite server. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | #### Output @@ -693,7 +667,6 @@ Retrieve REST web-services concurrency limits for the NetSuite account and integ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account | -| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential | #### Output diff --git a/apps/docs/content/docs/en/integrations/new_relic.mdx b/apps/docs/content/docs/en/integrations/new_relic.mdx index 04ec68c73bd..683110e51ff 100644 --- a/apps/docs/content/docs/en/integrations/new_relic.mdx +++ b/apps/docs/content/docs/en/integrations/new_relic.mdx @@ -44,7 +44,7 @@ Run a NRQL query against a New Relic account using NerdGraph. | `region` | string | No | New Relic data center region: us or eu | | `accountId` | number | Yes | New Relic account ID to query | | `nrql` | string | Yes | NRQL query to execute | -| `timeout` | number | No | Optional query timeout in seconds | +| `queryTimeout` | number | No | Optional query timeout in seconds | #### Output diff --git a/apps/docs/content/docs/en/integrations/pipedrive.mdx b/apps/docs/content/docs/en/integrations/pipedrive.mdx index 2dc380ceeef..1cda528615d 100644 --- a/apps/docs/content/docs/en/integrations/pipedrive.mdx +++ b/apps/docs/content/docs/en/integrations/pipedrive.mdx @@ -43,7 +43,6 @@ Retrieve all deals from Pipedrive with optional filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `status` | string | No | Only fetch deals with a specific status. Values: open, won, lost. If omitted, all not deleted deals are returned | | `person_id` | string | No | If supplied, only deals linked to the specified person are returned \(e.g., "456"\) | | `org_id` | string | No | If supplied, only deals linked to the specified organization are returned \(e.g., "789"\) | @@ -88,7 +87,6 @@ Retrieve detailed information about a specific deal | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `deal_id` | string | Yes | The ID of the deal to retrieve \(e.g., "123"\) | #### Output @@ -106,7 +104,6 @@ Create a new deal in Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `title` | string | Yes | The title of the deal \(e.g., "Enterprise Software License"\) | | `value` | string | No | The monetary value of the deal \(e.g., "5000"\) | | `currency` | string | No | Currency code \(e.g., "USD", "EUR", "GBP"\) | @@ -132,7 +129,6 @@ Update an existing deal in Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `deal_id` | string | Yes | The ID of the deal to update \(e.g., "123"\) | | `title` | string | No | New title for the deal \(e.g., "Updated Enterprise License"\) | | `value` | string | No | New monetary value for the deal \(e.g., "7500"\) | @@ -155,7 +151,6 @@ Retrieve files from Pipedrive with optional filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `sort` | string | No | Sort files by field \(supported: "id", "update_time"\) | | `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 100\) | | `start` | string | No | Pagination start offset \(0-based index of the first item to return\) | @@ -190,7 +185,6 @@ Retrieve mail threads from Pipedrive mailbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `folder` | string | No | Filter by folder: inbox, drafts, sent, archive \(default: inbox\) | | `limit` | string | No | Number of results to return \(e.g., "25", default: 50\) | | `start` | string | No | Pagination start offset \(0-based index of the first item to return\) | @@ -213,7 +207,6 @@ Retrieve all messages from a specific mail thread | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `thread_id` | string | Yes | The ID of the mail thread \(e.g., "12345"\) | #### Output @@ -232,7 +225,6 @@ Retrieve all pipelines from Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `sort_by` | string | No | Field to sort by: id, update_time, add_time \(default: id\) | | `sort_direction` | string | No | Sorting direction: asc, desc \(default: asc\) | | `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) | @@ -264,7 +256,6 @@ Retrieve all deals in a specific pipeline | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `pipeline_id` | string | Yes | The ID of the pipeline \(e.g., "1"\) | | `stage_id` | string | No | Filter by specific stage within the pipeline \(e.g., "2"\) | | `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) | @@ -286,7 +277,6 @@ Retrieve all projects or a specific project from Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `project_id` | string | No | Optional: ID of a specific project to retrieve \(e.g., "123"\) | | `status` | string | No | Filter by project status: open, completed, deleted \(only for listing all\) | | `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500, only for listing all\) | @@ -311,7 +301,6 @@ Create a new project in Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `title` | string | Yes | The title of the project \(e.g., "Q2 Marketing Campaign"\) | | `description` | string | No | Description of the project | | `start_date` | string | No | Project start date in YYYY-MM-DD format \(e.g., "2025-04-01"\) | @@ -332,7 +321,6 @@ Retrieve activities (tasks) from Pipedrive with optional filters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `user_id` | string | No | Filter activities by user ID \(e.g., "123"\) | | `type` | string | No | Filter by activity type \(call, meeting, task, deadline, email, lunch\) | | `done` | string | No | Filter by completion status: 0 for not done, 1 for done | @@ -370,7 +358,6 @@ Create a new activity (task) in Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `subject` | string | Yes | The subject/title of the activity \(e.g., "Follow up call with John"\) | | `type` | string | Yes | Activity type: call, meeting, task, deadline, email, lunch | | `due_date` | string | Yes | Due date in YYYY-MM-DD format \(e.g., "2025-03-15"\) | @@ -396,7 +383,6 @@ Update an existing activity (task) in Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `activity_id` | string | Yes | The ID of the activity to update \(e.g., "12345"\) | | `subject` | string | No | New subject/title for the activity \(e.g., "Updated meeting with client"\) | | `due_date` | string | No | New due date in YYYY-MM-DD format \(e.g., "2025-03-20"\) | @@ -420,7 +406,6 @@ Retrieve all leads or a specific lead from Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `lead_id` | string | No | Optional: ID of a specific lead to retrieve \(e.g., "abc123-def456-ghi789"\) | | `archived` | string | No | Get archived leads instead of active ones \(e.g., "true" or "false"\) | | `owner_id` | string | No | Filter by owner user ID \(e.g., "123"\) | @@ -474,7 +459,6 @@ Create a new lead in Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `title` | string | Yes | The name of the lead \(e.g., "Acme Corp - Website Redesign"\) | | `person_id` | string | No | ID of the person \(REQUIRED unless organization_id is provided\) \(e.g., "456"\) | | `organization_id` | string | No | ID of the organization \(REQUIRED unless person_id is provided\) \(e.g., "789"\) | @@ -499,7 +483,6 @@ Update an existing lead in Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `lead_id` | string | Yes | The ID of the lead to update \(e.g., "abc123-def456-ghi789"\) | | `title` | string | No | New name for the lead \(e.g., "Updated Lead - Premium Package"\) | | `person_id` | string | No | New person ID \(e.g., "456"\) | @@ -525,7 +508,6 @@ Delete a specific lead from Pipedrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `authStyle` | string | No | Auth scheme for the token; set by the credential resolver for API-token service accounts | | `lead_id` | string | Yes | The ID of the lead to delete \(e.g., "abc123-def456-ghi789"\) | #### Output diff --git a/apps/docs/content/docs/en/integrations/salesforce.mdx b/apps/docs/content/docs/en/integrations/salesforce.mdx index 4caf95090c2..70d28955364 100644 --- a/apps/docs/content/docs/en/integrations/salesforce.mdx +++ b/apps/docs/content/docs/en/integrations/salesforce.mdx @@ -41,11 +41,9 @@ Retrieve accounts from Salesforce CRM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | The ID token from Salesforce OAuth \(contains instance URL\) | -| `instanceUrl` | string | No | The Salesforce instance URL | -| `limit` | string | No | Maximum number of results \(default: 100, max: 2000\) | -| `fields` | string | No | Comma-separated field API names \(e.g., "Id,Name,Industry,Phone"\) | -| `orderBy` | string | No | Field and direction for sorting \(e.g., "Name ASC" or "CreatedDate DESC"\) | +| `limit` | string | No | Maximum number of results \(default: 100\) | +| `fields` | string | No | Comma-separated field API names \(e.g., "Id,Name,Industry,Phone"\) Also accepts FIELDS\(STANDARD\|CUSTOM\|ALL\) and toLabel\(\)/FORMAT\(\)/convertCurrency\(\) around a single field. | +| `orderBy` | string | No | Field and direction for sorting \(e.g., "Name ASC" or "CreatedDate DESC"\) Bare field API names only, optionally with ASC/DESC and NULLS FIRST/LAST; SOQL functions such as DISTANCE\(\) are not accepted here — use the Salesforce Query tool for those. | #### Output @@ -71,8 +69,6 @@ Create a new account in Salesforce CRM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `name` | string | Yes | Account name \(required\) | | `type` | string | No | Account type \(e.g., Customer, Partner, Prospect\) | | `industry` | string | No | Industry \(e.g., Technology, Healthcare, Finance\) | @@ -105,8 +101,6 @@ Update an existing account in Salesforce CRM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `accountId` | string | Yes | Salesforce Account ID to update \(18-character string starting with 001\) | | `name` | string | No | Account name | | `type` | string | No | Account type \(e.g., Customer, Partner, Prospect\) | @@ -139,8 +133,6 @@ Delete an account from Salesforce CRM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `accountId` | string | Yes | Salesforce Account ID to delete \(18-character string starting with 001\) | #### Output @@ -160,12 +152,10 @@ Get contact(s) from Salesforce - single contact if ID provided, or list if not | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `contactId` | string | No | Salesforce Contact ID \(18-character string starting with 003\) to get a single contact | -| `limit` | string | No | Maximum number of results \(default: 100, max: 2000\). Only for list query. | -| `fields` | string | No | Comma-separated field API names \(e.g., "Id,FirstName,LastName,Email,Phone"\) | -| `orderBy` | string | No | Field and direction for sorting \(e.g., "LastName ASC"\). Only for list query. | +| `limit` | string | No | Maximum number of results \(default: 100\). Only for list query. | +| `fields` | string | No | Comma-separated field API names \(e.g., "Id,FirstName,LastName,Email,Phone"\) Also accepts FIELDS\(STANDARD\|CUSTOM\|ALL\) and toLabel\(\)/FORMAT\(\)/convertCurrency\(\) around a single field. | +| `orderBy` | string | No | Field and direction for sorting \(e.g., "LastName ASC"\). Only for list query. Bare field API names only, optionally with ASC/DESC and NULLS FIRST/LAST; SOQL functions such as DISTANCE\(\) are not accepted here — use the Salesforce Query tool for those. | #### Output @@ -193,8 +183,6 @@ Create a new contact in Salesforce CRM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `lastName` | string | Yes | Last name \(required\) | | `firstName` | string | No | First name | | `email` | string | No | Email address | @@ -227,8 +215,6 @@ Update an existing contact in Salesforce CRM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `contactId` | string | Yes | Salesforce Contact ID to update \(18-character string starting with 003\) | | `lastName` | string | No | Last name | | `firstName` | string | No | First name | @@ -261,8 +247,6 @@ Delete a contact from Salesforce CRM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `contactId` | string | Yes | Salesforce Contact ID to delete \(18-character string starting with 003\) | #### Output @@ -282,12 +266,10 @@ Retrieve lead(s) from Salesforce CRM | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `leadId` | string | No | Salesforce Lead ID \(18-character string starting with 00Q\) to get a single lead | | `limit` | string | No | Maximum number of results to return \(default: 100\) | -| `fields` | string | No | Comma-separated list of field API names to return | -| `orderBy` | string | No | Field and direction for sorting \(e.g., LastName ASC\) | +| `fields` | string | No | Comma-separated list of field API names to return Also accepts FIELDS\(STANDARD\|CUSTOM\|ALL\) and toLabel\(\)/FORMAT\(\)/convertCurrency\(\) around a single field. | +| `orderBy` | string | No | Field and direction for sorting \(e.g., LastName ASC\) Bare field API names only, optionally with ASC/DESC and NULLS FIRST/LAST; SOQL functions such as DISTANCE\(\) are not accepted here — use the Salesforce Query tool for those. | #### Output @@ -315,8 +297,6 @@ Create a new lead | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `lastName` | string | Yes | Last name \(required\) | | `company` | string | Yes | Company name \(required\) | | `firstName` | string | No | First name | @@ -345,8 +325,6 @@ Update an existing lead | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `leadId` | string | Yes | Salesforce Lead ID to update \(18-character string starting with 00Q\) | | `lastName` | string | No | Last name | | `company` | string | No | Company name | @@ -375,8 +353,6 @@ Delete a lead | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `leadId` | string | Yes | Salesforce Lead ID to delete \(18-character string starting with 00Q\) | #### Output @@ -396,12 +372,10 @@ Get opportunity(ies) from Salesforce | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `opportunityId` | string | No | Salesforce Opportunity ID \(18-character string starting with 006\) to get a single opportunity | | `limit` | string | No | Maximum number of results to return \(default: 100\) | -| `fields` | string | No | Comma-separated list of field API names to return | -| `orderBy` | string | No | Field and direction for sorting \(e.g., CloseDate DESC\) | +| `fields` | string | No | Comma-separated list of field API names to return Also accepts FIELDS\(STANDARD\|CUSTOM\|ALL\) and toLabel\(\)/FORMAT\(\)/convertCurrency\(\) around a single field. | +| `orderBy` | string | No | Field and direction for sorting \(e.g., CloseDate DESC\) Bare field API names only, optionally with ASC/DESC and NULLS FIRST/LAST; SOQL functions such as DISTANCE\(\) are not accepted here — use the Salesforce Query tool for those. | #### Output @@ -428,8 +402,6 @@ Create a new opportunity | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `name` | string | Yes | Opportunity name \(required\) | | `stageName` | string | Yes | Stage name \(required, e.g., Prospecting, Qualification, Closed Won\) | | `closeDate` | string | Yes | Close date in YYYY-MM-DD format \(required\) | @@ -456,8 +428,6 @@ Update an existing opportunity | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `opportunityId` | string | Yes | Salesforce Opportunity ID to update \(18-character string starting with 006\) | | `name` | string | No | Opportunity name | | `stageName` | string | No | Stage name \(e.g., Prospecting, Qualification, Closed Won\) | @@ -484,8 +454,6 @@ Delete an opportunity | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `opportunityId` | string | Yes | Salesforce Opportunity ID to delete \(18-character string starting with 006\) | #### Output @@ -505,12 +473,10 @@ Get case(s) from Salesforce | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `caseId` | string | No | Salesforce Case ID \(18-character string starting with 500\) to get a single case | | `limit` | string | No | Maximum number of results to return \(default: 100\) | -| `fields` | string | No | Comma-separated list of field API names to return | -| `orderBy` | string | No | Field and direction for sorting \(e.g., CreatedDate DESC\) | +| `fields` | string | No | Comma-separated list of field API names to return Also accepts FIELDS\(STANDARD\|CUSTOM\|ALL\) and toLabel\(\)/FORMAT\(\)/convertCurrency\(\) around a single field. | +| `orderBy` | string | No | Field and direction for sorting \(e.g., CreatedDate DESC\) Bare field API names only, optionally with ASC/DESC and NULLS FIRST/LAST; SOQL functions such as DISTANCE\(\) are not accepted here — use the Salesforce Query tool for those. | #### Output @@ -537,8 +503,6 @@ Create a new case | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `subject` | string | Yes | Case subject \(required\) | | `status` | string | No | Status \(e.g., New, Working, Escalated\) | | `priority` | string | No | Priority \(e.g., Low, Medium, High\) | @@ -565,8 +529,6 @@ Update an existing case | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `caseId` | string | Yes | Salesforce Case ID to update \(18-character string starting with 500\) | | `subject` | string | No | Case subject | | `status` | string | No | Status \(e.g., New, Working, Escalated, Closed\) | @@ -593,8 +555,6 @@ Delete a case | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `caseId` | string | Yes | Salesforce Case ID to delete \(18-character string starting with 500\) | #### Output @@ -614,12 +574,10 @@ Get task(s) from Salesforce | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `taskId` | string | No | Salesforce Task ID \(18-character string starting with 00T\) to get a single task | | `limit` | string | No | Maximum number of results to return \(default: 100\) | -| `fields` | string | No | Comma-separated list of field API names to return | -| `orderBy` | string | No | Field and direction for sorting \(e.g., ActivityDate DESC\) | +| `fields` | string | No | Comma-separated list of field API names to return Also accepts FIELDS\(STANDARD\|CUSTOM\|ALL\) and toLabel\(\)/FORMAT\(\)/convertCurrency\(\) around a single field. | +| `orderBy` | string | No | Field and direction for sorting \(e.g., ActivityDate DESC\) Bare field API names only, optionally with ASC/DESC and NULLS FIRST/LAST; SOQL functions such as DISTANCE\(\) are not accepted here — use the Salesforce Query tool for those. | #### Output @@ -646,8 +604,6 @@ Create a new task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `subject` | string | Yes | Task subject \(required\) | | `status` | string | No | Status \(e.g., Not Started, In Progress, Completed\) | | `priority` | string | No | Priority \(e.g., Low, Normal, High\) | @@ -674,8 +630,6 @@ Update an existing task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `taskId` | string | Yes | Salesforce Task ID to update \(18-character string starting with 00T\) | | `subject` | string | No | Task subject | | `status` | string | No | Status \(e.g., Not Started, In Progress, Completed\) | @@ -702,8 +656,6 @@ Delete a task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `taskId` | string | Yes | Salesforce Task ID to delete \(18-character string starting with 00T\) | #### Output @@ -723,8 +675,6 @@ Get a list of up to 200 recently viewed reports for the current user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `searchTerm` | string | No | Filter reports by name \(case-insensitive partial match\) | #### Output @@ -745,8 +695,6 @@ Get the describe (definition and metadata) for a specific report | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `reportId` | string | Yes | Salesforce Report ID \(18-character string starting with 00O\) | #### Output @@ -767,8 +715,6 @@ Execute a report and retrieve the results | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `reportId` | string | Yes | Salesforce Report ID \(18-character string starting with 00O\) | | `includeDetails` | string | No | Include detail rows \(true/false, default: true\) | | `filters` | string | No | JSON array of report filter objects to apply | @@ -799,8 +745,6 @@ Get a list of available report types | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | #### Output @@ -820,8 +764,6 @@ Get a list of recently used dashboards for the current user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | #### Output @@ -841,8 +783,6 @@ Get details and results for a specific dashboard | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `dashboardId` | string | Yes | Salesforce Dashboard ID \(18-character string starting with 01Z\) | #### Output @@ -867,8 +807,6 @@ Refresh a dashboard to get the latest data | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `dashboardId` | string | Yes | Salesforce Dashboard ID \(18-character string starting with 01Z\) | #### Output @@ -894,8 +832,6 @@ Execute a custom SOQL query to retrieve data from Salesforce | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `query` | string | Yes | SOQL query to execute \(e.g., SELECT Id, Name FROM Account LIMIT 10\) | #### Output @@ -919,8 +855,6 @@ Retrieve additional query results using the nextRecordsUrl from a previous query | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `nextRecordsUrl` | string | Yes | The nextRecordsUrl value from a previous query response \(e.g., /services/data/v59.0/query/01g...\) | #### Output @@ -943,8 +877,6 @@ Get metadata and field information for a Salesforce object | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `objectName` | string | Yes | Salesforce object API name \(e.g., Account, Contact, Lead, Custom_Object__c\) | #### Output @@ -998,8 +930,6 @@ Get a list of all available Salesforce objects | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | #### Output @@ -1037,8 +967,6 @@ Create a custom field on a Salesforce object (e.g., Account) using the Tooling A | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `objectName` | string | Yes | API name of the object to add the field to \(e.g., Account, Contact, Lead, MyObject__c\) | | `fieldName` | string | Yes | API name of the new field; the __c suffix is added automatically \(e.g., Region\) | | `label` | string | No | Display label shown in the UI \(defaults to the field name when omitted\) | @@ -1074,8 +1002,6 @@ Update an existing custom field on a Salesforce object using the Tooling API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `fieldId` | string | Yes | Tooling API Id of the custom field to update \(find it via the Tooling Query tool\) | | `label` | string | No | Display label shown in the UI | | `length` | number | No | Maximum length for Text, LongTextArea, Html, or MultiselectPicklist fields | @@ -1107,8 +1033,6 @@ Delete a custom field from a Salesforce object using the Tooling API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `fieldId` | string | Yes | Tooling API Id of the custom field to delete \(find it via the Tooling Query tool\) | #### Output @@ -1128,8 +1052,6 @@ Create a custom object in Salesforce using the Tooling API | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `objectName` | string | Yes | API name of the new object; the __c suffix is added automatically \(e.g., Project\) | | `label` | string | Yes | Singular display label for the object \(e.g., Project\) | | `pluralLabel` | string | Yes | Plural display label for the object \(e.g., Projects\) | @@ -1156,8 +1078,6 @@ Execute a SOQL query against the Tooling API to inspect metadata objects | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `idToken` | string | No | No description | -| `instanceUrl` | string | No | No description | | `query` | string | Yes | Tooling SOQL query \(e.g., SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId = 'Account'\) | #### Output diff --git a/apps/docs/content/docs/en/integrations/serper.mdx b/apps/docs/content/docs/en/integrations/serper.mdx index cd070d4b073..338f8c6d65a 100644 --- a/apps/docs/content/docs/en/integrations/serper.mdx +++ b/apps/docs/content/docs/en/integrations/serper.mdx @@ -35,7 +35,7 @@ Integrate Serper into the workflow. Can search the web. ### Web Search -A powerful web search tool that provides access to Google search results through Serper.dev API. Supports different types of searches including regular web search, news, places, images, videos, and shopping. Returns comprehensive results including organic results, knowledge graph, answer box, people also ask, related searches, and top stories. +Search Google through the Serper.dev API. Supports the web, news, places, images, videos, shopping, scholar, and patents verticals, and returns a flat list of results for the requested vertical with its type-specific metadata (date and source for news, rating, review count, address, coordinates, category, phone, and website for places, image and thumbnail URLs for images, duration and channel for videos, price, delivery, and rating for shopping). The web vertical additionally returns the knowledge graph, "people also ask", and related searches when Google renders them. #### Input @@ -52,18 +52,42 @@ A powerful web search tool that provides access to Google search results through | Parameter | Type | Description | | --------- | ---- | ----------- | -| `searchResults` | array | Search results with titles, links, snippets, and type-specific metadata \(date for news, rating for places, imageUrl for images, duration/source for videos, price/source for shopping\) | +| `searchResults` | array | Results for the requested vertical, with titles, links, snippets, and type-specific metadata \(date/source for news, rating/ratingCount/address/latitude/longitude/category/phoneNumber/website for places, imageUrl/thumbnailUrl/source for images, duration/channel/source for videos, price/delivery/rating/ratingCount/source for shopping\). Places, images, and shopping results have no snippet; places results have no link. | | ↳ `title` | string | Result title | -| ↳ `link` | string | Result URL | -| ↳ `snippet` | string | Result description/snippet | +| ↳ `link` | string | Result URL. Absent for places results, which have no result URL | +| ↳ `snippet` | string | Result description/snippet \(search/news/videos/scholar/patents\). The images, shopping, and places endpoints return no snippet | | ↳ `position` | number | Position in search results | -| ↳ `date` | string | Publication date \(news/videos\) | -| ↳ `imageUrl` | string | Image URL \(images/news/shopping\) | -| ↳ `source` | string | Source name \(news/videos/shopping\) | -| ↳ `rating` | number | Rating \(places\) | -| ↳ `ratingCount` | number | Number of reviews \(places\) | +| ↳ `date` | string | Publication date \(search/news/videos\) | +| ↳ `imageUrl` | string | Image URL \(images/news/videos/shopping\) | +| ↳ `thumbnailUrl` | string | Thumbnail image URL \(images\) | +| ↳ `source` | string | Source name \(images/news/videos/shopping\) | +| ↳ `channel` | string | Publishing channel \(videos\) | +| ↳ `rating` | number | Average rating, 1-5 \(places/shopping\) | +| ↳ `ratingCount` | number | Number of ratings/reviews \(places/shopping\) | | ↳ `address` | string | Address \(places\) | +| ↳ `latitude` | number | Latitude of the place \(places\) | +| ↳ `longitude` | number | Longitude of the place \(places\) | +| ↳ `category` | string | Primary category, e.g. "Coffee shop" \(places\) | +| ↳ `phoneNumber` | string | Contact phone number \(places\) | +| ↳ `website` | string | Business website URL \(places\) | | ↳ `price` | string | Price \(shopping\) | +| ↳ `delivery` | string | Delivery/shipping cost as Google reports it \(shopping\) | | ↳ `duration` | string | Duration \(videos\) | +| `knowledgeGraph` | object | Google knowledge panel for the query. Only returned by the web search vertical, and only when Google renders a panel. | +| ↳ `title` | string | Entity name | +| ↳ `type` | string | Entity type, e.g. "Website" | +| ↳ `website` | string | Entity website URL | +| ↳ `imageUrl` | string | Entity image URL | +| ↳ `description` | string | Entity description | +| ↳ `descriptionSource` | string | Publication the description was taken from, e.g. "Wikipedia" | +| ↳ `descriptionLink` | string | URL of the description source | +| ↳ `attributes` | object | Key/value facts Google lists in the panel | +| `peopleAlsoAsk` | array | Google "People also ask" entries. Only returned by the web search vertical, and only when Google renders the block. | +| ↳ `question` | string | The question asked | +| ↳ `snippet` | string | Answer snippet | +| ↳ `title` | string | Title of the answering page | +| ↳ `link` | string | URL of the answering page | +| `relatedSearches` | array | Google "Related searches" queries. Only returned by the web search vertical, and only when Google renders the block. | +| ↳ `query` | string | The related query text | diff --git a/apps/docs/content/docs/en/integrations/sixtyfour.mdx b/apps/docs/content/docs/en/integrations/sixtyfour.mdx index 844c3ac495b..8d06fc086fe 100644 --- a/apps/docs/content/docs/en/integrations/sixtyfour.mdx +++ b/apps/docs/content/docs/en/integrations/sixtyfour.mdx @@ -101,8 +101,9 @@ Enrich lead information with contact details, social profiles, and company data | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Sixtyfour API key | | `leadInfo` | string | Yes | Lead information as JSON object with key-value pairs \(e.g. name, company, title, linkedin\) | -| `struct` | string | Yes | Fields to collect as JSON object. Keys are field names, values are descriptions \(e.g. \{"email": "The individual\'s email address", "phone": "Phone number"\}\) | +| `struct` | string | No | Optional fields to collect as JSON object. Keys are field names, values are descriptions \(e.g. \{"email": "The individual\'s email address", "phone": "Phone number"\}\). Only the fields listed here are returned in structured_data — there is no default field set, so omitting this returns an empty structured_data and only the research notes, references, and confidence score. | | `researchPlan` | string | No | Optional research plan to guide enrichment strategy | +| `tier` | string | No | Research depth and credit spend: micro, low, medium, high, or xhigh. Defaults to low when omitted; micro is cheaper than the default. | #### Output @@ -129,6 +130,7 @@ Enrich company data with additional information and find associated people using | `researchPlan` | string | No | Optional strategy describing how the agent should search for information | | `peopleFocusPrompt` | string | No | Description of people to find \(roles, responsibilities\) | | `leadStruct` | string | No | Custom schema for returned lead data as JSON object | +| `tier` | string | No | Research depth and credit spend: micro, low, medium, or high. Defaults to low when omitted; micro is cheaper than the default. | #### Output diff --git a/apps/docs/content/docs/en/integrations/slack.mdx b/apps/docs/content/docs/en/integrations/slack.mdx index c2ff9abf2af..56af83e283d 100644 --- a/apps/docs/content/docs/en/integrations/slack.mdx +++ b/apps/docs/content/docs/en/integrations/slack.mdx @@ -163,7 +163,6 @@ Create and share Slack canvases in channels. Canvases are collaborative document | `channel` | string | Yes | Slack channel ID \(e.g., C1234567890\) | | `title` | string | Yes | Title of the canvas | | `content` | string | Yes | Canvas content in markdown format | -| `document_content` | object | No | Structured canvas document content | #### Output diff --git a/apps/docs/content/docs/en/integrations/snowflake.mdx b/apps/docs/content/docs/en/integrations/snowflake.mdx index f10f6d15e9b..37931f22f79 100644 --- a/apps/docs/content/docs/en/integrations/snowflake.mdx +++ b/apps/docs/content/docs/en/integrations/snowflake.mdx @@ -45,7 +45,6 @@ Execute one parameterized SQL statement through the Snowflake SQL API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -93,7 +92,6 @@ Check a running or completed statement and retrieve exactly one result partition | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `statementHandle` | string | Yes | Statement handle returned by Snowflake | | `partition` | number | No | Zero-based result partition to retrieve; defaults to 0 | | `partitionCount` | number | No | Total number of result partitions, taken from the partitionCount of the first partition. Snowflake omits metadata from every later partition response, so supply this when fetching partition 1 or higher to keep truncated and nextPartition accurate | @@ -135,7 +133,6 @@ Cancel a running Snowflake SQL API statement. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `statementHandle` | string | Yes | Statement handle returned by Snowflake | #### Output @@ -175,7 +172,6 @@ Insert structured JSON rows using bound values. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -221,7 +217,6 @@ Update matching rows with a bound MERGE statement without inserting new rows. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -268,7 +263,6 @@ Update matching rows and insert unmatched rows with a bound MERGE statement. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -315,7 +309,6 @@ Delete rows matching a required set of bound column filters. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -361,7 +354,6 @@ Load files from an existing Snowflake stage with COPY INTO. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -414,7 +406,6 @@ Export a Snowflake table to files in a stage with COPY INTO. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -466,7 +457,6 @@ List the databases the credential can access. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `nameLike` | string | No | Optional SQL LIKE pattern for object names | @@ -509,7 +499,6 @@ List the schemas in a Snowflake database. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -553,7 +542,6 @@ List the tables in a Snowflake schema. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -598,7 +586,6 @@ List warehouses visible to the active Snowflake role. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `maxRows` | number | No | Maximum result rows; defaults to 1000 with a Sim safety limit of 10000 | @@ -641,7 +628,6 @@ Get the full details for a Snowflake virtual warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouseName` | string | Yes | Warehouse name | @@ -683,7 +669,6 @@ Resume a Snowflake virtual warehouse if it is suspended. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouseName` | string | Yes | Warehouse name | @@ -725,7 +710,6 @@ Suspend a Snowflake virtual warehouse. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouseName` | string | Yes | Warehouse name | @@ -767,7 +751,6 @@ Resize a Snowflake warehouse or change its auto-suspend and auto-resume settings | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouseName` | string | Yes | Warehouse name | @@ -812,7 +795,6 @@ List tasks in a Snowflake schema. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -857,7 +839,6 @@ Describe a Snowflake task. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -901,7 +882,6 @@ Run a Snowflake task immediately, optionally retrying its last failed graph. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -946,7 +926,6 @@ Resume a suspended Snowflake task so its schedule runs again. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -990,7 +969,6 @@ Suspend a Snowflake task so its schedule stops triggering runs. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `database` | string | Yes | Database name | @@ -1034,7 +1012,6 @@ Query up to seven days of Snowflake task history, capped at 10000 rows. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1081,7 +1058,6 @@ Find one task history record by query ID within Snowflake’s seven-day window a | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1127,7 +1103,6 @@ Cancel one running task query by query ID with SYSTEM$CANCEL_QUERY. Task runs al | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1170,7 +1145,6 @@ Read a task query result with RESULT_SCAN during Snowflake’s 24-hour retention | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1214,7 +1188,6 @@ List queries that completed in the last seven days, optionally filtered. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1262,7 +1235,6 @@ List staged-file load results for a table over the last fourteen days. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1310,7 +1282,6 @@ Inspect table and column metadata through Snowflake INFORMATION_SCHEMA views. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | @@ -1357,7 +1328,6 @@ Call a stored procedure with explicitly typed Snowflake bindings. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) | -| `domain` | string | No | Snowflake account host injected by the executor from the selected credential | | `role` | string | No | Snowflake role to use for this statement | | `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds | | `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting | diff --git a/apps/docs/content/docs/en/integrations/textract.mdx b/apps/docs/content/docs/en/integrations/textract.mdx index 17b5bc046c4..31d73374b02 100644 --- a/apps/docs/content/docs/en/integrations/textract.mdx +++ b/apps/docs/content/docs/en/integrations/textract.mdx @@ -39,7 +39,6 @@ Integrate AWS Textract into your workflow to extract text, tables, forms, and ke | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `file` | file | No | Document to be processed \(JPEG, PNG, or single-page PDF\). | #### Output @@ -90,7 +89,6 @@ Extract structured invoice and receipt fields using AWS Textract AnalyzeExpense | `secretAccessKey` | string | Yes | AWS Secret Access Key | | `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | | `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. | -| `file` | file | No | Invoice or receipt to be processed \(JPEG, PNG, or single-page PDF\). | | `filePath` | string | No | URL to an invoice or receipt to be processed, if not uploaded directly. | | `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). | @@ -120,10 +118,7 @@ Extract identity document fields using AWS Textract AnalyzeID | `accessKeyId` | string | Yes | AWS Access Key ID | | `secretAccessKey` | string | Yes | AWS Secret Access Key | | `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | -| `file` | file | No | Front of the identity document \(JPEG, PNG, or PDF\). | | `filePath` | string | No | URL to the front of the identity document, if not uploaded directly. | -| `fileBack` | file | No | Back of the identity document, if applicable \(JPEG, PNG, or PDF\). | -| `filePathBack` | string | No | URL to the back of the identity document, if not uploaded directly. | #### Output diff --git a/apps/docs/content/docs/en/integrations/trigger_dev.mdx b/apps/docs/content/docs/en/integrations/trigger_dev.mdx index 5a15f07ffc4..fffae974d60 100644 --- a/apps/docs/content/docs/en/integrations/trigger_dev.mdx +++ b/apps/docs/content/docs/en/integrations/trigger_dev.mdx @@ -185,6 +185,7 @@ Retrieve a Trigger.dev run by its ID, including status, payload, output, attempt | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -262,6 +263,7 @@ Retrieve the result of a Trigger.dev run: whether it succeeded, its output, and | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -456,6 +458,7 @@ Reschedule a delayed Trigger.dev run with a new delay. Only valid while the run | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -573,6 +576,7 @@ Create an imperative cron schedule that triggers a Trigger.dev task on a recurri | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -650,6 +654,7 @@ Retrieve a Trigger.dev schedule by its ID. | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -767,6 +772,7 @@ Update an imperative Trigger.dev schedule by its ID, replacing its task, cron ex | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -862,6 +868,7 @@ Activate an imperative Trigger.dev schedule so it resumes triggering its task. | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -939,6 +946,7 @@ Deactivate an imperative Trigger.dev schedule so it stops triggering its task. | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -961,7 +969,7 @@ Deactivate an imperative Trigger.dev schedule so it stops triggering its task. ### Trigger.dev List Env Vars -List the environment variables of a Trigger.dev project environment. Values are returned in plaintext and will appear in workflow outputs and run history — scope this operation carefully. +List the environment variables of a Trigger.dev project environment. Secret variables are returned redacted; non-secret values are returned in plaintext and will appear in workflow outputs and run history — scope this operation carefully. #### Input @@ -977,7 +985,8 @@ List the environment variables of a Trigger.dev project environment. Values are | --------- | ---- | ----------- | | `variables` | array | Environment variables in the project environment | | ↳ `name` | string | Name of the environment variable | -| ↳ `value` | string | Plaintext value of the environment variable; appears in workflow outputs and run history | +| ↳ `value` | string | Value of the environment variable. Secret variables come back redacted, not as the real value; non-secret values are plaintext and appear in workflow outputs and run history | +| ↳ `isSecret` | boolean | Whether the variable is a secret. A secret variable's value comes back redacted, so branch on this before treating the value as real | ### Trigger.dev Create Env Var @@ -1002,7 +1011,7 @@ Create an environment variable in a Trigger.dev project environment. ### Trigger.dev Get Env Var -Retrieve an environment variable from a Trigger.dev project environment. The value is returned in plaintext and will appear in workflow outputs and run history. +Retrieve an environment variable from a Trigger.dev project environment. A secret variable is returned redacted; a non-secret value is returned in plaintext and will appear in workflow outputs and run history. #### Input @@ -1018,7 +1027,8 @@ Retrieve an environment variable from a Trigger.dev project environment. The val | Parameter | Type | Description | | --------- | ---- | ----------- | | `name` | string | Name of the environment variable | -| `value` | string | Plaintext value of the environment variable; appears in workflow outputs and run history | +| `value` | string | Value of the environment variable. Secret variables come back redacted, not as the real value; non-secret values are plaintext and appear in workflow outputs and run history | +| `isSecret` | boolean | Whether the variable is a secret. A secret variable's value comes back redacted, so branch on this before treating the value as real | ### Trigger.dev Update Env Var @@ -1140,6 +1150,7 @@ Retrieve a Trigger.dev queue by ID, task identifier, or custom queue name, inclu | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -1252,6 +1263,7 @@ Pause a Trigger.dev queue so no new runs start. Runs that are currently executin | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -1330,6 +1342,7 @@ Resume a paused Trigger.dev queue so new runs can start again. | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -1409,6 +1422,7 @@ Override the concurrency limit of a Trigger.dev queue with a new value. | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -1487,6 +1501,7 @@ Reset the concurrency limit of a Trigger.dev queue back to its base value, remov | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -1588,6 +1603,7 @@ Retrieve a Trigger.dev deployment by its ID, including its status, version, and | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -1664,6 +1680,7 @@ Retrieve the latest Trigger.dev deployment in the environment of the API key. | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | @@ -1761,7 +1778,7 @@ Create a Trigger.dev waitpoint token that a task can wait on until it is complet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Trigger.dev secret API key \(starts with tr_\) | -| `timeout` | string | No | How long before the token times out, as a duration \("30s", "1m", "2h", "3d"\) or an ISO 8601 date | +| `tokenTimeout` | string | No | How long before the token times out, as a duration \("30s", "1m", "2h", "3d"\) or an ISO 8601 date | | `idempotencyKey` | string | No | Idempotency key; passing the same key before it expires returns the original token | | `idempotencyKeyTTL` | string | No | How long the idempotency key is valid, as a duration \("30s", "1m", "2h", "3d"\) | | `tags` | string | No | Comma-separated tags to attach to the waitpoint \(max 10, each under 128 characters\) | @@ -1849,6 +1866,7 @@ Retrieve a Trigger.dev waitpoint token by its ID, including its status, timeout, | `variables` | json | Environment variables in the project environment \(List Env Vars\) | | `name` | string | Environment variable or queue name \(env var and queue operations\) | | `value` | string | Value of the environment variable \(Get Env Var\) | +| `isSecret` | boolean | Whether the environment variable is a secret, meaning its value is redacted \(Get Env Var\) | | `success` | boolean | Whether the operation succeeded \(env var operations, Complete Waitpoint Token\) | | `count` | number | Number of environment variables submitted \(Import Env Vars\) | | `queues` | json | Queues in the environment \(List Queues\) | diff --git a/apps/docs/content/docs/en/integrations/twilio_voice.mdx b/apps/docs/content/docs/en/integrations/twilio_voice.mdx index b3bd30b57d5..daeacc33089 100644 --- a/apps/docs/content/docs/en/integrations/twilio_voice.mdx +++ b/apps/docs/content/docs/en/integrations/twilio_voice.mdx @@ -52,7 +52,7 @@ Make an outbound phone call using Twilio Voice API. | `authToken` | string | Yes | Twilio Auth Token | | `record` | boolean | No | Whether to record the call | | `recordingStatusCallback` | string | No | Webhook URL for recording status updates | -| `timeout` | number | No | Time to wait for answer before giving up \(seconds, default: 60\) | +| `callTimeout` | number | No | Time to wait for answer before giving up \(seconds, default: 60\) | | `machineDetection` | string | No | Answering machine detection: Enable or DetectMessageEnd | #### Output diff --git a/apps/docs/content/docs/en/integrations/vanta.mdx b/apps/docs/content/docs/en/integrations/vanta.mdx index 7ae2463a694..090157448ba 100644 --- a/apps/docs/content/docs/en/integrations/vanta.mdx +++ b/apps/docs/content/docs/en/integrations/vanta.mdx @@ -330,7 +330,6 @@ Upload an evidence file to a Vanta document. Requires credentials with the vanta | `region` | string | No | Vanta API region: "us" \(api.vanta.com, default\) or "gov" \(api.vanta-gov.com\) | | `documentId` | string | Yes | Unique ID of the document to attach the file to | | `file` | file | No | The evidence file to upload | -| `fileContent` | string | No | Base64-encoded file content \(alternative to file\) | | `fileName` | string | No | Optional file name override | | `mimeType` | string | No | MIME type of the file \(e.g., application/pdf\); used when uploading base64 content, since uploaded files already carry their own type | | `description` | string | No | Description of the uploaded evidence \(e.g., "Q3 access review evidence"\) | diff --git a/apps/docs/content/docs/en/integrations/vercel.mdx b/apps/docs/content/docs/en/integrations/vercel.mdx index c8a66c405fe..21759f233ac 100644 --- a/apps/docs/content/docs/en/integrations/vercel.mdx +++ b/apps/docs/content/docs/en/integrations/vercel.mdx @@ -97,7 +97,7 @@ Get details of a specific Vercel deployment | `id` | string | Deployment ID | | `name` | string | Deployment name | | `url` | string | Unique deployment URL | -| `readyState` | string | Deployment ready state: QUEUED, BUILDING, ERROR, INITIALIZING, READY, CANCELED | +| `readyState` | string | Deployment ready state: BLOCKED, BUILDING, CANCELED, ERROR, INITIALIZING, QUEUED, or READY | | `status` | string | Deployment status | | `target` | string | Target environment | | `createdAt` | number | Creation timestamp in milliseconds | @@ -156,7 +156,7 @@ Create a new deployment or redeploy an existing one | `id` | string | Deployment ID | | `name` | string | Deployment name | | `url` | string | Unique deployment URL | -| `readyState` | string | Deployment ready state: QUEUED, BUILDING, ERROR, INITIALIZING, READY, CANCELED | +| `readyState` | string | Deployment ready state: BLOCKED, BUILDING, CANCELED, ERROR, INITIALIZING, QUEUED, or READY | | `projectId` | string | Associated project ID | | `createdAt` | number | Creation timestamp in milliseconds | | `alias` | array | Assigned aliases | @@ -1260,7 +1260,7 @@ Create, update, upsert, or delete items in an Edge Config store | Parameter | Type | Description | | --------- | ---- | ----------- | -| `status` | string | Operation status | +| `status` | string | Operation status reported by Vercel | ### Vercel Delete Edge Config diff --git a/apps/docs/content/docs/en/integrations/zoho_desk.mdx b/apps/docs/content/docs/en/integrations/zoho_desk.mdx index d7b58c75448..b7694afaff4 100644 --- a/apps/docs/content/docs/en/integrations/zoho_desk.mdx +++ b/apps/docs/content/docs/en/integrations/zoho_desk.mdx @@ -51,7 +51,6 @@ List tickets from a Zoho Desk organization with optional filters. Returns a list | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `from` | number | No | Pagination start index \(0-based\) | | `limit` | number | No | Number of tickets to return \(1-100\) | @@ -111,7 +110,6 @@ Retrieve a single Zoho Desk ticket by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `ticketId` | string | Yes | Ticket ID to retrieve | | `include` | string | No | Comma-separated related data to embed. Allowed: contacts, products, assignee, departments, contract, isRead, team, skills | @@ -162,7 +160,6 @@ Update fields on an existing Zoho Desk ticket. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `ticketId` | string | Yes | Ticket ID to update | | `subject` | string | No | Ticket subject | @@ -224,7 +221,6 @@ List comments on a Zoho Desk ticket. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `ticketId` | string | Yes | Ticket ID | | `from` | number | No | Pagination start index \(0-based\) | @@ -267,7 +263,6 @@ Add a comment to a Zoho Desk ticket. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `ticketId` | string | Yes | Ticket ID | | `content` | string | Yes | Comment content | @@ -309,7 +304,6 @@ List conversation threads on a Zoho Desk ticket, newest first (Zoho sorts by sen | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `ticketId` | string | Yes | Ticket ID | | `from` | number | No | Pagination start index \(0-based\) | @@ -367,7 +361,6 @@ Retrieve the full content of a single Zoho Desk ticket thread. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `ticketId` | string | Yes | Ticket ID | | `threadId` | string | Yes | Thread ID | @@ -423,7 +416,6 @@ Retrieve a Zoho Desk contact by ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `contactId` | string | Yes | Contact ID to retrieve | | `include` | string | No | Comma-separated related data to embed. Allowed: accounts, owner | @@ -460,7 +452,6 @@ Download a Zoho Desk ticket attachment (from its href) as a file. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | | `orgId` | string | Yes | Zoho Desk organization ID | | `href` | string | Yes | Attachment download href \(from a thread or comment attachment\) | | `fileName` | string | No | Optional file name for the downloaded file | @@ -479,7 +470,6 @@ List the Zoho Desk organizations (portals) the connected account can access. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiDomain` | string | No | Zoho Desk data-center REST base URL | #### Output diff --git a/apps/sim/app/api/tools/google_drive/download/path-safety.test.ts b/apps/sim/app/api/tools/google_drive/download/path-safety.test.ts new file mode 100644 index 00000000000..26ca333cef5 --- /dev/null +++ b/apps/sim/app/api/tools/google_drive/download/path-safety.test.ts @@ -0,0 +1,161 @@ +/** + * @vitest-environment node + * + * Guards the internal Google Drive download route against path traversal. + * + * This route was invisible to the tools-side traversal sweep, which probed + * each tool's `request.url`. The route has none: the `google_drive_download` + * tool posts to it and the route builds the googleapis URL itself, bare — + * `https://www.googleapis.com/drive/v3/files/${fileId}` — from a body field + * the contract validates only as a non-empty string, and which the tool + * declares `visibility: 'user-or-llm'`. + * + * Every assertion resolves the built URL through `new URL(...)`, the same + * normalization `fetch` performs, and checks the resolved pathname's segment + * count and fixed segments rather than a `startsWith` prefix — which + * `/drive/v3/files/..` would still satisfy before normalization. + */ +import { + createMockRequest, + hybridAuthMockFns, + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { POST } from '@/app/api/tools/google_drive/download/route' + +const { mockValidateUrlWithDNS, mockSecureFetchWithPinnedIP } = inputValidationMockFns + +const PINNED_IP = '93.184.216.34' + +/** Values no encoding neutralizes; the route must reject them outright. */ +const REJECTED = ['..', '.', ' .. ', 'a/b', '..\\..'] as const + +/** Values that must not throw but must stay inside one path segment. */ +const NEUTRALIZED = ['%2e%2e', 'x?alt=media'] as const + +function jsonResponse(body: unknown) { + return { + ok: true, + status: 200, + statusText: '', + headers: new Headers(), + body: null, + text: async () => JSON.stringify(body), + json: async () => body, + arrayBuffer: async () => new ArrayBuffer(0), + } +} + +function fileResponse() { + return { + ok: true, + status: 200, + statusText: '', + headers: new Headers(), + body: null, + text: async () => '', + json: async () => ({}), + arrayBuffer: async () => new ArrayBuffer(8), + } +} + +function metadataFor(fileId: string) { + return { + id: fileId, + name: 'report.pdf', + mimeType: 'application/pdf', + size: '8', + capabilities: { canReadRevisions: false }, + } +} + +function requestedUrls(): string[] { + return mockValidateUrlWithDNS.mock.calls.map((call) => String(call[0])) +} + +beforeEach(() => { + vi.clearAllMocks() + hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + authType: 'internal_jwt', + }) + mockValidateUrlWithDNS.mockResolvedValue({ + isValid: true, + resolvedIP: PINNED_IP, + originalHostname: 'www.googleapis.com', + }) +}) + +describe('POST /api/tools/google_drive/download traversal safety', () => { + it.each(REJECTED)( + 'rejects fileId %j with a clean 400 and no outbound request', + async (fileId) => { + const response = await POST(createMockRequest('POST', { accessToken: 'token-123', fileId })) + + expect(response.status).toBe(400) + const data = (await response.json()) as { success: boolean; error: string } + expect(data.success).toBe(false) + expect(data.error).toMatch(/fileId/) + expect(mockValidateUrlWithDNS).not.toHaveBeenCalled() + expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + } + ) + + it.each(NEUTRALIZED)('keeps fileId %j inside a single path segment', async (fileId) => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(jsonResponse(metadataFor('file-abc'))) + .mockResolvedValueOnce(fileResponse()) + + const response = await POST(createMockRequest('POST', { accessToken: 'token-123', fileId })) + expect(response.status).toBe(200) + + const metadataUrl = new URL(requestedUrls()[0]) + const segments = metadataUrl.pathname.split('/').filter(Boolean) + expect(segments).toHaveLength(4) + expect(segments.slice(0, 3)).toEqual(['drive', 'v3', 'files']) + expect(decodeURIComponent(segments[3])).toBe(fileId) + expect(metadataUrl.searchParams.get('supportsAllDrives')).toBe('true') + }) + + it('leaves a legitimate file id byte-identical to the pre-guard URL', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(jsonResponse(metadataFor('1a2B3c4D-5e6F_7g8H9i0J'))) + .mockResolvedValueOnce(fileResponse()) + + const response = await POST( + createMockRequest('POST', { accessToken: 'token-123', fileId: '1a2B3c4D-5e6F_7g8H9i0J' }) + ) + expect(response.status).toBe(200) + + const urls = requestedUrls() + expect(urls[0]).toContain('/drive/v3/files/1a2B3c4D-5e6F_7g8H9i0J?fields=') + expect(new URL(urls[1]).pathname).toBe('/drive/v3/files/1a2B3c4D-5e6F_7g8H9i0J') + }) + + it('preserves a dot inside a longer id and keeps the revisions path intact', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce( + jsonResponse({ ...metadataFor('a..b'), capabilities: { canReadRevisions: true } }) + ) + .mockResolvedValueOnce(fileResponse()) + .mockResolvedValueOnce(jsonResponse({ revisions: [] })) + + const response = await POST( + createMockRequest('POST', { + accessToken: 'token-123', + fileId: 'a..b', + includeRevisions: true, + }) + ) + expect(response.status).toBe(200) + + const revisionsUrl = new URL(requestedUrls()[2]) + const segments = revisionsUrl.pathname.split('/').filter(Boolean) + expect(segments).toEqual(['drive', 'v3', 'files', 'a..b', 'revisions']) + }) +}) diff --git a/apps/sim/app/api/tools/google_drive/download/route.ts b/apps/sim/app/api/tools/google_drive/download/route.ts index 788a7de418b..489890a45ea 100644 --- a/apps/sim/app/api/tools/google_drive/download/route.ts +++ b/apps/sim/app/api/tools/google_drive/download/route.ts @@ -20,6 +20,7 @@ import { GOOGLE_WORKSPACE_MIME_TYPES, VALID_EXPORT_FORMATS, } from '@/tools/google_drive/utils' +import { safeUrlPathSegment } from '@/tools/url-path' export const dynamic = 'force-dynamic' @@ -83,9 +84,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => { rawExportMimeType && rawExportMimeType !== 'auto' ? rawExportMimeType : null const authHeader = `Bearer ${accessToken}` + let fileIdSegment: string + try { + fileIdSegment = safeUrlPathSegment(fileId, 'fileId') + } catch (error) { + logger.warn(`[${requestId}] Rejected unsafe fileId`, { + error: getErrorMessage(error, 'Invalid fileId'), + }) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Invalid fileId') }, + { status: 400 } + ) + } + logger.info(`[${requestId}] Getting file metadata from Google Drive`, { fileId }) - const metadataUrl = `https://www.googleapis.com/drive/v3/files/${fileId}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true` + const metadataUrl = `https://www.googleapis.com/drive/v3/files/${fileIdSegment}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true` const metadataUrlValidation = await validateUrlWithDNS(metadataUrl, 'metadataUrl') if (!metadataUrlValidation.isValid) { return NextResponse.json( @@ -150,7 +164,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { exportFormat, }) - const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(exportFormat)}&supportsAllDrives=true` + const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileIdSegment}/export?mimeType=${encodeURIComponent(exportFormat)}&supportsAllDrives=true` const exportUrlValidation = await validateUrlWithDNS(exportUrl, 'exportUrl') if (!exportUrlValidation.isValid) { return NextResponse.json( @@ -194,7 +208,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } } - const downloadUrl = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&supportsAllDrives=true` + const downloadUrl = `https://www.googleapis.com/drive/v3/files/${fileIdSegment}?alt=media&supportsAllDrives=true` const downloadUrlValidation = await validateUrlWithDNS(downloadUrl, 'downloadUrl') if (!downloadUrlValidation.isValid) { return NextResponse.json( @@ -230,7 +244,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const canReadRevisions = metadata.capabilities?.canReadRevisions === true if (includeRevisions && canReadRevisions) { try { - const revisionsUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/revisions?fields=revisions(${ALL_REVISION_FIELDS})&pageSize=100` + const revisionsUrl = `https://www.googleapis.com/drive/v3/files/${fileIdSegment}/revisions?fields=revisions(${ALL_REVISION_FIELDS})&pageSize=100` const revisionsUrlValidation = await validateUrlWithDNS(revisionsUrl, 'revisionsUrl') if (revisionsUrlValidation.isValid) { const revisionsResponse = await secureFetchWithPinnedIP( diff --git a/apps/sim/app/api/tools/google_drive/export/path-safety.test.ts b/apps/sim/app/api/tools/google_drive/export/path-safety.test.ts new file mode 100644 index 00000000000..cd6952658b4 --- /dev/null +++ b/apps/sim/app/api/tools/google_drive/export/path-safety.test.ts @@ -0,0 +1,150 @@ +/** + * @vitest-environment node + * + * Guards the internal Google Drive export route against path traversal. + * + * This route already wrapped `fileId` in `encodeURIComponent`, which is not a + * fix: `.` and `..` are unreserved characters, so they survive encoding, and + * the WHATWG parser then removes them as dot segments *after* percent-decoding + * (`/drive/v3/files/%2e%2e/export` resolves to `/drive/v3/export`). Only value + * rejection closes it. + * + * Assertions resolve through `new URL(...)` and check the resolved pathname's + * segment count and fixed segments, never a `startsWith` prefix. + */ +import { + createMockRequest, + hybridAuthMockFns, + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { POST } from '@/app/api/tools/google_drive/export/route' + +const { mockValidateUrlWithDNS, mockSecureFetchWithPinnedIP } = inputValidationMockFns + +const PINNED_IP = '93.184.216.34' +const DOC_MIME = 'application/vnd.google-apps.document' +const EXPORT_MIME = 'text/plain' + +const REJECTED = ['..', '.', ' .. ', 'a/b', '..\\..'] as const +const NEUTRALIZED = ['%2e%2e', 'x?alt=media'] as const + +function metadataResponse(fileId: string) { + const body = { id: fileId, name: 'notes', mimeType: DOC_MIME } + return { + ok: true, + status: 200, + statusText: '', + headers: new Headers(), + body: null, + text: async () => JSON.stringify(body), + json: async () => body, + arrayBuffer: async () => new ArrayBuffer(0), + } +} + +function exportResponse() { + return { + ok: true, + status: 200, + statusText: '', + headers: new Headers({ 'content-length': '4' }), + body: null, + text: async () => 'text', + json: async () => ({}), + arrayBuffer: async () => new ArrayBuffer(4), + } +} + +function requestedUrls(): string[] { + return mockValidateUrlWithDNS.mock.calls.map((call) => String(call[0])) +} + +function bodyFor(fileId: string) { + return { accessToken: 'token-123', fileId, mimeType: EXPORT_MIME } +} + +beforeEach(() => { + vi.clearAllMocks() + hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + authType: 'internal_jwt', + }) + mockValidateUrlWithDNS.mockResolvedValue({ + isValid: true, + resolvedIP: PINNED_IP, + originalHostname: 'www.googleapis.com', + }) +}) + +describe('POST /api/tools/google_drive/export traversal safety', () => { + it.each(REJECTED)( + 'rejects fileId %j with a clean 400 and no outbound request', + async (fileId) => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(metadataResponse('doc-1')) + .mockResolvedValueOnce(exportResponse()) + + const response = await POST(createMockRequest('POST', bodyFor(fileId))) + + expect(response.status).toBe(400) + const data = (await response.json()) as { success: boolean; error: string } + expect(data.success).toBe(false) + expect(data.error).toMatch(/fileId/) + expect(mockValidateUrlWithDNS).not.toHaveBeenCalled() + expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + } + ) + + it.each(NEUTRALIZED)('keeps fileId %j inside a single path segment', async (fileId) => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(metadataResponse('doc-1')) + .mockResolvedValueOnce(exportResponse()) + + const response = await POST(createMockRequest('POST', bodyFor(fileId))) + expect(response.status).toBe(200) + + const metadataSegments = new URL(requestedUrls()[0]).pathname.split('/').filter(Boolean) + expect(metadataSegments).toHaveLength(4) + expect(metadataSegments.slice(0, 3)).toEqual(['drive', 'v3', 'files']) + expect(decodeURIComponent(metadataSegments[3])).toBe(fileId) + + const exportSegments = new URL(requestedUrls()[1]).pathname.split('/').filter(Boolean) + expect(exportSegments).toHaveLength(5) + expect(exportSegments.slice(0, 3)).toEqual(['drive', 'v3', 'files']) + expect(decodeURIComponent(exportSegments[3])).toBe(fileId) + expect(exportSegments[4]).toBe('export') + }) + + it('leaves a legitimate file id byte-identical to the pre-guard URL', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(metadataResponse('1a2B3c4D-5e6F_7g8H9i0J')) + .mockResolvedValueOnce(exportResponse()) + + const response = await POST(createMockRequest('POST', bodyFor('1a2B3c4D-5e6F_7g8H9i0J'))) + expect(response.status).toBe(200) + + const urls = requestedUrls() + expect(urls[0]).toContain('/drive/v3/files/1a2B3c4D-5e6F_7g8H9i0J?fields=') + expect(urls[1]).toBe( + 'https://www.googleapis.com/drive/v3/files/1a2B3c4D-5e6F_7g8H9i0J/export?mimeType=text%2Fplain' + ) + }) + + it('preserves a dot inside a longer id', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(metadataResponse('a..b')) + .mockResolvedValueOnce(exportResponse()) + + const response = await POST(createMockRequest('POST', bodyFor('a..b'))) + expect(response.status).toBe(200) + + const exportSegments = new URL(requestedUrls()[1]).pathname.split('/').filter(Boolean) + expect(exportSegments).toEqual(['drive', 'v3', 'files', 'a..b', 'export']) + }) +}) diff --git a/apps/sim/app/api/tools/google_drive/export/route.ts b/apps/sim/app/api/tools/google_drive/export/route.ts index 40b48057def..d6b414c2d0a 100644 --- a/apps/sim/app/api/tools/google_drive/export/route.ts +++ b/apps/sim/app/api/tools/google_drive/export/route.ts @@ -17,6 +17,7 @@ import { MAX_EXPORT_BYTES, VALID_EXPORT_FORMATS, } from '@/tools/google_drive/utils' +import { safeUrlPathSegment } from '@/tools/url-path' export const dynamic = 'force-dynamic' @@ -62,9 +63,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const { accessToken, fileId, mimeType: exportMimeType, fileName } = parsed.data.body const authHeader = `Bearer ${accessToken}` + let fileIdSegment: string + try { + fileIdSegment = safeUrlPathSegment(fileId, 'fileId') + } catch (error) { + logger.warn(`[${requestId}] Rejected unsafe fileId`, { + error: getErrorMessage(error, 'Invalid fileId'), + }) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Invalid fileId') }, + { status: 400 } + ) + } + logger.info(`[${requestId}] Getting file metadata from Google Drive`, { fileId }) - const metadataUrl = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true` + const metadataUrl = `https://www.googleapis.com/drive/v3/files/${fileIdSegment}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true` const metadataUrlValidation = await validateUrlWithDNS(metadataUrl, 'metadataUrl') if (!metadataUrlValidation.isValid) { return NextResponse.json( @@ -123,7 +137,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { exportFormat: exportMimeType, }) - const exportUrl = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}/export?mimeType=${encodeURIComponent(exportMimeType)}` + const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileIdSegment}/export?mimeType=${encodeURIComponent(exportMimeType)}` const exportUrlValidation = await validateUrlWithDNS(exportUrl, 'exportUrl') if (!exportUrlValidation.isValid) { return NextResponse.json( diff --git a/apps/sim/app/api/tools/google_vault/download-export-file/path-safety.test.ts b/apps/sim/app/api/tools/google_vault/download-export-file/path-safety.test.ts new file mode 100644 index 00000000000..38a57530f7a --- /dev/null +++ b/apps/sim/app/api/tools/google_vault/download-export-file/path-safety.test.ts @@ -0,0 +1,243 @@ +/** + * @vitest-environment node + * + * Guards the Google Vault export-file download route against a dot-segment + * pop, WITHOUT weakening its `%2F` encoding. + * + * A GCS object name legitimately contains `/`, and the JSON API requires it + * percent-encoded as `%2F` inside the single `/o/{object}` segment. So + * `safeUrlPath` (which preserves `/` as a separator) would be the wrong tool + * here and `encodeURIComponent` must stay. What `encodeURIComponent` does not + * stop is a value that is exactly `.` or `..`: those characters are + * unreserved, survive encoding, and the WHATWG parser removes the segment + * afterwards — `/b/{bucket}/o/..` resolves to `/b/{bucket}/`, the object + * *list* endpoint, with the caller's bearer token attached. + * + * Only the whole value is checked, not each `/`-separated component: because + * the value becomes one `%2F`-encoded segment, an interior `..` never forms a + * URL segment and an object literally named `a/../b` is legally addressable. + * + * `objectName` and `bucketName` are `visibility: 'user-only'` on + * `google_vault_download_export_file`, so this is not LLM-reachable — lower + * severity than the Drive `fileId` sites, fixed for the same reason. + */ +import { + createMockRequest, + hybridAuthMockFns, + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { POST } from '@/app/api/tools/google_vault/download-export-file/route' + +const { mockValidateUrlWithDNS, mockSecureFetchWithPinnedIP } = inputValidationMockFns + +const PINNED_IP = '93.184.216.34' +const REJECTED = ['..', '.'] as const + +/** + * Whitespace-padded dot segments. GCS documents any Unicode character as legal + * in an object name, so `' ..'` is a real, addressable object — and the server + * does not trim, so `' ..'` and `'..'` are *different* objects. Rejecting the + * padded form (which trimming before the comparison did) is a false rejection, + * and rewriting it would silently address the wrong object. It is safe to allow + * because `encodeURIComponent` turns the padding into `%20`, and the WHATWG + * parser only removes a segment that is *exactly* `.` or `..` — the same + * argument `safeUrlPath`'s `preserveOuterWhitespace` option records for + * Supabase Storage keys. + */ +const PADDED_DOT_NAMES = [' ..', '.. ', ' .. ', ' . ', '\t..'] as const + +function downloadResponse() { + return { + ok: true, + status: 200, + statusText: '', + headers: new Headers({ 'content-type': 'application/zip' }), + body: null, + text: async () => '', + json: async () => ({}), + arrayBuffer: async () => new ArrayBuffer(4), + } +} + +function requestedUrl(): string { + return String(mockValidateUrlWithDNS.mock.calls[0][0]) +} + +beforeEach(() => { + vi.clearAllMocks() + hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + authType: 'internal_jwt', + }) + mockValidateUrlWithDNS.mockResolvedValue({ + isValid: true, + resolvedIP: PINNED_IP, + originalHostname: 'storage.googleapis.com', + }) +}) + +describe('POST /api/tools/google_vault/download-export-file traversal safety', () => { + it.each(REJECTED)( + 'rejects objectName %j with a clean 400 and no outbound request', + async (objectName) => { + mockSecureFetchWithPinnedIP.mockResolvedValueOnce(downloadResponse()) + + const response = await POST( + createMockRequest('POST', { + accessToken: 'token-123', + matterId: 'matter-1', + bucketName: 'vault-bucket', + objectName, + }) + ) + + expect(response.status).toBe(400) + const data = (await response.json()) as { success: boolean; error: string } + expect(data.success).toBe(false) + expect(data.error).toMatch(/objectName/) + expect(mockValidateUrlWithDNS).not.toHaveBeenCalled() + expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + } + ) + + it.each(REJECTED)( + 'rejects bucketName %j with a clean 400 and no outbound request', + async (bucketName) => { + mockSecureFetchWithPinnedIP.mockResolvedValueOnce(downloadResponse()) + + const response = await POST( + createMockRequest('POST', { + accessToken: 'token-123', + matterId: 'matter-1', + bucketName, + objectName: 'exports/file.zip', + }) + ) + + expect(response.status).toBe(400) + const data = (await response.json()) as { success: boolean; error: string } + expect(data.success).toBe(false) + expect(data.error).toMatch(/bucketName/) + expect(mockValidateUrlWithDNS).not.toHaveBeenCalled() + expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + } + ) + + it('keeps a nested object name as one %2F-encoded segment, byte-identical to today', async () => { + mockSecureFetchWithPinnedIP.mockResolvedValueOnce(downloadResponse()) + + const response = await POST( + createMockRequest('POST', { + accessToken: 'token-123', + matterId: 'matter-1', + bucketName: 'vault-bucket', + objectName: 'matter-1/exports/file name.zip', + }) + ) + expect(response.status).toBe(200) + + expect(requestedUrl()).toBe( + 'https://storage.googleapis.com/storage/v1/b/vault-bucket/o/matter-1%2Fexports%2Ffile%20name.zip?alt=media' + ) + const segments = new URL(requestedUrl()).pathname.split('/').filter(Boolean) + expect(segments).toEqual([ + 'storage', + 'v1', + 'b', + 'vault-bucket', + 'o', + 'matter-1%2Fexports%2Ffile%20name.zip', + ]) + }) + + it.each(PADDED_DOT_NAMES)( + 'accepts the legal padded object name %j and keeps the path shape intact', + async (objectName) => { + mockSecureFetchWithPinnedIP.mockResolvedValueOnce(downloadResponse()) + + const response = await POST( + createMockRequest('POST', { + accessToken: 'token-123', + matterId: 'matter-1', + bucketName: 'vault-bucket', + objectName, + }) + ) + expect(response.status).toBe(200) + + const url = new URL(requestedUrl()) + expect(url.pathname.split('/')).toEqual([ + '', + 'storage', + 'v1', + 'b', + 'vault-bucket', + 'o', + encodeURIComponent(objectName), + ]) + expect(decodeURIComponent(url.pathname.split('/')[6])).toBe(objectName) + } + ) + + it.each(PADDED_DOT_NAMES)( + 'accepts the legal padded bucket name %j and keeps the path shape intact', + async (bucketName) => { + mockSecureFetchWithPinnedIP.mockResolvedValueOnce(downloadResponse()) + + const response = await POST( + createMockRequest('POST', { + accessToken: 'token-123', + matterId: 'matter-1', + bucketName, + objectName: 'exports/file.zip', + }) + ) + expect(response.status).toBe(200) + + const url = new URL(requestedUrl()) + expect(url.pathname.split('/')).toEqual([ + '', + 'storage', + 'v1', + 'b', + encodeURIComponent(bucketName), + 'o', + 'exports%2Ffile.zip', + ]) + } + ) + + it('proves the padded forms cannot pop a segment while the bare forms can', () => { + const build = (value: string) => + new URL(`https://storage.googleapis.com/storage/v1/b/bkt/o/${encodeURIComponent(value)}`) + + for (const padded of PADDED_DOT_NAMES) { + expect(build(padded).pathname.split('/')).toHaveLength(7) + } + expect(build('..').pathname.split('/')).toHaveLength(6) + expect(build('.').pathname.split('/')[6]).toBe('') + }) + + it('preserves an interior ".." component, which never forms a URL segment', async () => { + mockSecureFetchWithPinnedIP.mockResolvedValueOnce(downloadResponse()) + + const response = await POST( + createMockRequest('POST', { + accessToken: 'token-123', + matterId: 'matter-1', + bucketName: 'vault-bucket', + objectName: 'a/../b', + }) + ) + expect(response.status).toBe(200) + + const segments = new URL(requestedUrl()).pathname.split('/').filter(Boolean) + expect(segments).toEqual(['storage', 'v1', 'b', 'vault-bucket', 'o', 'a%2F..%2Fb']) + }) +}) diff --git a/apps/sim/app/api/tools/google_vault/download-export-file/route.ts b/apps/sim/app/api/tools/google_vault/download-export-file/route.ts index 9a492698afa..8a6b9743bd1 100644 --- a/apps/sim/app/api/tools/google_vault/download-export-file/route.ts +++ b/apps/sim/app/api/tools/google_vault/download-export-file/route.ts @@ -16,6 +16,33 @@ export const dynamic = 'force-dynamic' const logger = createLogger('GoogleVaultDownloadExportFileAPI') +/** + * Rejects a value that would collapse the URL path segment it occupies. + * + * This route deliberately keeps `encodeURIComponent` on the object name: a GCS + * object name legitimately contains `/`, and the JSON API addresses it as a + * single segment with those slashes as `%2F`, so the multi-segment + * `safeUrlPath` helper would misaddress the object. But encoding never + * neutralizes a dot segment — `.` and `..` are unreserved, so they survive + * encoding and the WHATWG parser removes the segment afterwards, turning + * `/b/{bucket}/o/..` into the object *list* endpoint with the caller's bearer + * token attached. Only rejection closes that, and only the whole value can do + * it: an interior `..` is encoded into the same segment and stays inert. + * + * The comparison is against the **raw** value, deliberately not a trimmed one. + * GCS documents any Unicode character as legal in an object name and does not + * trim, so `' ..'` is a real object distinct from `'..'`; rejecting it is a + * false rejection of a legal name. It is also unnecessary — the padding + * encodes to `%20`/`%09` and the WHATWG parser removes only a segment that is + * *exactly* `.` or `..`, so `%20..` stays inert text. This is the same + * reasoning `safeUrlPath` records for its `preserveOuterWhitespace` option. + */ +function assertNotDotSegment(value: string, paramName: string): void { + if (value === '.' || value === '..') { + throw new Error(`${paramName} cannot be "${value}" (path traversal is not allowed)`) + } +} + export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() @@ -39,6 +66,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const { accessToken, bucketName, objectName, fileName } = validatedData + try { + assertNotDotSegment(bucketName, 'bucketName') + assertNotDotSegment(objectName, 'objectName') + } catch (error) { + const message = getErrorMessage(error, 'Invalid request') + logger.warn(`[${requestId}] Rejected unsafe Vault object path`, { error: message }) + return NextResponse.json({ success: false, error: message }, { status: 400 }) + } + const bucket = encodeURIComponent(bucketName) const object = encodeURIComponent(objectName) const downloadUrl = `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}?alt=media` diff --git a/apps/sim/app/api/tools/hubspot/path_safety.test.ts b/apps/sim/app/api/tools/hubspot/path_safety.test.ts new file mode 100644 index 00000000000..b88c680b60b --- /dev/null +++ b/apps/sim/app/api/tools/hubspot/path_safety.test.ts @@ -0,0 +1,160 @@ +/** + * @vitest-environment node + * + * The HubSpot `properties` and `pipelines` selector routes assemble their + * provider URL *here*, from a query parameter, so the reflective `request.url` + * probe in `tools/hubspot/path_safety.test.ts` cannot see them. + * + * `objectType` is genuinely caller-supplied — the contract constrains it only + * to a non-empty string, and the selector resolves it from the free-text + * `customObjectTypeId` trigger field. `BUILT_IN_PATH` maps the four known + * slugs to a safe constant, but the `?? objectType` fallback puts the raw value + * straight into a path segment. `encodeURIComponent` does not help: `.` and + * `..` are unreserved, and the WHATWG parser removes a dot segment *after* + * percent-decoding, so `objectType='..'` re-aims the request one level up with + * the caller's HubSpot bearer token still attached. + * + * Every assertion resolves the outgoing URL through `new URL(...)`, the same + * normalization `fetch` applies, and compares whole-pathname segment shape + * rather than the template text. + */ +import { createMockRequest } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockAuthorizeCredentialUse, mockRefreshAccessToken } = vi.hoisted(() => ({ + mockAuthorizeCredentialUse: vi.fn(), + mockRefreshAccessToken: vi.fn(), +})) + +vi.mock('@/lib/auth/credential-access', () => ({ + authorizeCredentialUse: mockAuthorizeCredentialUse, +})) + +vi.mock('@/lib/oauth/credential-service', () => ({ + refreshAccessTokenIfNeeded: mockRefreshAccessToken, +})) + +import { GET as GET_PIPELINES } from '@/app/api/tools/hubspot/pipelines/route' +import { GET as GET_PROPERTIES } from '@/app/api/tools/hubspot/properties/route' + +const ORIGIN = 'https://api.hubapi.com' +const CREDENTIAL_ID = 'cred-1' + +/** + * Values that must be rejected outright. `' .. '` is included because a + * padded dot segment is only inert if nothing later trims it. + */ +const REJECTED = ['..', '.', '%2e%2e', 'a/b', ' .. ', '..%2f..', '../..'] as const + +/** + * The three legal `objectType` spellings from HubSpot's Schemas API — + * portal-qualified custom object, `{meta-type}-{unique id}`, and a bare custom + * object name — plus the built-in slug that maps through `BUILT_IN_PATH`. + */ +const LEGITIMATE = ['p7878787_my_object', '2-123456', '0-1', 'my_object'] as const + +/** + * Keys that exist on `Object.prototype`, not on `BUILT_IN_PATH`. A plain object + * literal resolves them through the prototype chain, so `BUILT_IN_PATH[key]` + * returns a function or an object rather than `undefined` — the `?? objectType` + * fallback never fires, and `validatePathSegment` then calls `.includes` on a + * non-string and throws, turning caller-controlled input into a 500. Every one + * of these is a syntactically legal HubSpot custom-object name, so the correct + * behavior is to treat it as one: pass it through the validator as the string + * it is. + */ +const PROTOTYPE_KEYS = [ + 'constructor', + '__proto__', + 'toString', + 'hasOwnProperty', + 'valueOf', +] as const + +let fetchMock: ReturnType + +beforeEach(() => { + vi.clearAllMocks() + mockAuthorizeCredentialUse.mockResolvedValue({ + ok: true, + credentialOwnerUserId: 'user-1', + resolvedCredentialId: CREDENTIAL_ID, + }) + mockRefreshAccessToken.mockResolvedValue('inert-token') + fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ results: [] }), + text: async () => '', + }) + vi.stubGlobal('fetch', fetchMock) +}) + +function request(path: string, objectType: string) { + const url = new URL(`http://localhost:3000/api/tools/hubspot/${path}`) + url.searchParams.set('credentialId', CREDENTIAL_ID) + url.searchParams.set('objectType', objectType) + return createMockRequest('GET', undefined, {}, url.toString()) +} + +function outgoingUrl(): URL { + expect(fetchMock).toHaveBeenCalledTimes(1) + return new URL(fetchMock.mock.calls[0][0] as string) +} + +const ROUTES = [ + { name: 'properties', handler: GET_PROPERTIES, collection: 'properties' }, + { name: 'pipelines', handler: GET_PIPELINES, collection: 'pipelines' }, +] as const + +describe.each(ROUTES)( + 'GET /api/tools/hubspot/$name path safety', + ({ name, handler, collection }) => { + it('maps a built-in slug to its documented plural constant', async () => { + const response = await handler(request(name, 'contact')) + expect(response.status).toBe(200) + + const url = outgoingUrl() + expect(url.origin).toBe(ORIGIN) + expect(url.pathname.split('/')).toEqual(['', 'crm', 'v3', collection, 'contacts']) + expect([...url.searchParams.keys()]).toEqual([]) + }) + + it.each(LEGITIMATE)('passes %j through byte-identically', async (objectType) => { + const response = await handler(request(name, objectType)) + expect(response.status).toBe(200) + + const url = outgoingUrl() + expect(fetchMock.mock.calls[0][0]).toBe(`${ORIGIN}/crm/v3/${collection}/${objectType}`) + expect(url.pathname.split('/')).toEqual(['', 'crm', 'v3', collection, objectType]) + }) + + it.each(PROTOTYPE_KEYS)( + 'treats the inherited key %j as a plain object-type string, never a 500', + async (objectType) => { + const response = await handler(request(name, objectType)) + + expect(response.status).not.toBe(500) + expect(response.status).toBe(200) + expect(outgoingUrl().pathname.split('/')).toEqual([ + '', + 'crm', + 'v3', + collection, + encodeURIComponent(objectType), + ]) + } + ) + + it.each(REJECTED)( + 'rejects objectType=%j with a 400 and never calls HubSpot', + async (objectType) => { + const response = await handler(request(name, objectType)) + + expect(response.status).toBe(400) + expect((await response.json()).error).toMatch(/objectType/) + expect(fetchMock).not.toHaveBeenCalled() + } + ) + } +) diff --git a/apps/sim/app/api/tools/hubspot/pipelines/route.ts b/apps/sim/app/api/tools/hubspot/pipelines/route.ts index c7fd92edaa1..6674340a5e3 100644 --- a/apps/sim/app/api/tools/hubspot/pipelines/route.ts +++ b/apps/sim/app/api/tools/hubspot/pipelines/route.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { hubspotPipelinesSelectorContract } from '@/lib/api/contracts/selectors/hubspot' import { parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { validateAlphanumericId, validatePathSegment } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/lib/oauth/credential-service' @@ -12,6 +12,21 @@ export const dynamic = 'force-dynamic' const logger = createLogger('HubSpotPipelinesAPI') +/** + * Built-in object slugs map to a safe plural constant; anything else falls + * through to the caller-supplied `objectType`, which the contract constrains + * only to a non-empty string. That value lands in a path segment, so it is + * validated before use — `encodeURIComponent` alone leaves a `.`/`..` segment + * intact and the WHATWG parser removes it, re-aiming the authenticated request. + * + * The lookup goes through `Object.hasOwn` rather than plain indexing: on a + * plain object literal, `objectType = 'constructor'` (or `'__proto__'`, + * `'toString'`, …) resolves through the prototype chain to a function, the + * `?? objectType` fallback never fires, and `validatePathSegment` then calls a + * string method on a non-string and throws — turning caller-controlled input + * into a 500. Those names are legal HubSpot custom-object spellings, so they + * must reach the validator as the strings they are. + */ const BUILT_IN_PATH: Record = { contact: 'contacts', company: 'companies', @@ -57,7 +72,15 @@ export const GET = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) } - const pathSegment = BUILT_IN_PATH[objectType] ?? objectType + const pathSegment = Object.hasOwn(BUILT_IN_PATH, objectType) + ? BUILT_IN_PATH[objectType] + : objectType + const pathSegmentValidation = validatePathSegment(pathSegment, { paramName: 'objectType' }) + if (!pathSegmentValidation.isValid) { + logger.warn(`[${requestId}] Invalid objectType: ${pathSegmentValidation.error}`) + return NextResponse.json({ error: pathSegmentValidation.error }, { status: 400 }) + } + const response = await fetch( `https://api.hubapi.com/crm/v3/pipelines/${encodeURIComponent(pathSegment)}`, { headers: { Authorization: `Bearer ${accessToken}` } } diff --git a/apps/sim/app/api/tools/hubspot/properties/route.ts b/apps/sim/app/api/tools/hubspot/properties/route.ts index 3d52b9b0d8f..66af8deb212 100644 --- a/apps/sim/app/api/tools/hubspot/properties/route.ts +++ b/apps/sim/app/api/tools/hubspot/properties/route.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { hubspotPropertiesSelectorContract } from '@/lib/api/contracts/selectors/hubspot' import { parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { validateAlphanumericId, validatePathSegment } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/lib/oauth/credential-service' @@ -12,6 +12,21 @@ export const dynamic = 'force-dynamic' const logger = createLogger('HubSpotPropertiesAPI') +/** + * Built-in object slugs map to a safe plural constant; anything else falls + * through to the caller-supplied `objectType`, which the contract constrains + * only to a non-empty string. That value lands in a path segment, so it is + * validated before use — `encodeURIComponent` alone leaves a `.`/`..` segment + * intact and the WHATWG parser removes it, re-aiming the authenticated request. + * + * The lookup goes through `Object.hasOwn` rather than plain indexing: on a + * plain object literal, `objectType = 'constructor'` (or `'__proto__'`, + * `'toString'`, …) resolves through the prototype chain to a function, the + * `?? objectType` fallback never fires, and `validatePathSegment` then calls a + * string method on a non-string and throws — turning caller-controlled input + * into a 500. Those names are legal HubSpot custom-object spellings, so they + * must reach the validator as the strings they are. + */ const BUILT_IN_PATH: Record = { contact: 'contacts', company: 'companies', @@ -60,7 +75,15 @@ export const GET = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) } - const pathSegment = BUILT_IN_PATH[objectType] ?? objectType + const pathSegment = Object.hasOwn(BUILT_IN_PATH, objectType) + ? BUILT_IN_PATH[objectType] + : objectType + const pathSegmentValidation = validatePathSegment(pathSegment, { paramName: 'objectType' }) + if (!pathSegmentValidation.isValid) { + logger.warn(`[${requestId}] Invalid objectType: ${pathSegmentValidation.error}`) + return NextResponse.json({ error: pathSegmentValidation.error }, { status: 400 }) + } + const response = await fetch( `https://api.hubapi.com/crm/v3/properties/${encodeURIComponent(pathSegment)}`, { headers: { Authorization: `Bearer ${accessToken}` } } diff --git a/apps/sim/app/api/tools/jira/add-attachment/route.test.ts b/apps/sim/app/api/tools/jira/add-attachment/route.test.ts new file mode 100644 index 00000000000..8af81d389b2 --- /dev/null +++ b/apps/sim/app/api/tools/jira/add-attachment/route.test.ts @@ -0,0 +1,172 @@ +/** + * @vitest-environment node + * + * `add-attachment` is an internal Next.js route, so the reflective + * `request.url` probe in `tools/jira/path_safety.test.ts` cannot see it: the + * provider URL is assembled *here*, from body fields, not by the tool config. + * + * Both `cloudId` and `issueKey` land in a path segment on a POST that carries + * the caller's OAuth bearer token and a multipart body. `encodeURIComponent` + * would not help — `.` and `..` are unreserved and the WHATWG parser removes + * them after decoding — so the sibling routes reject instead, and so must this + * one. Every assertion resolves the outgoing URL through `new URL(...)`, the + * same normalization `fetch` applies. + */ +import { createMockRequest, hybridAuthMockFns } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockProcessFilesToUserFiles, mockDownload, mockAssertToolFileAccess, mockGetJiraCloudId } = + vi.hoisted(() => ({ + mockProcessFilesToUserFiles: vi.fn(), + mockDownload: vi.fn(), + mockAssertToolFileAccess: vi.fn(), + mockGetJiraCloudId: vi.fn(), + })) + +vi.mock('@/lib/uploads/utils/file-utils', () => ({ + processFilesToUserFiles: mockProcessFilesToUserFiles, + isInternalFileUrl: () => true, +})) + +vi.mock('@/lib/uploads/utils/file-utils.server', () => ({ + downloadServableFileFromStorage: mockDownload, +})) + +vi.mock('@/app/api/files/authorization', () => ({ + assertToolFileAccess: mockAssertToolFileAccess, +})) + +vi.mock('@/lib/uploads/utils/servable-file-response', () => ({ + docNotReadyResponse: () => null, +})) + +vi.mock('@/tools/jira/utils', () => ({ + getJiraCloudId: mockGetJiraCloudId, + parseAtlassianErrorMessage: (status: number) => `Jira error ${status}`, +})) + +import { POST } from '@/app/api/tools/jira/add-attachment/route' + +const CLOUD_ID = '1324a887-45db-1bf4-1e99-ef0ff456d421' +const ORIGIN = 'https://api.atlassian.com' + +const FILE = { + key: 'workspace/u1/report.pdf', + name: 'report.pdf', + size: 12, + type: 'application/pdf', +} + +function body(overrides: Record = {}) { + return { + accessToken: 'inert-token', + domain: 'example.atlassian.net', + issueKey: 'PROJ-123', + cloudId: CLOUD_ID, + files: [FILE], + ...overrides, + } +} + +let fetchMock: ReturnType + +beforeEach(() => { + vi.clearAllMocks() + hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + authType: 'internal_jwt', + }) + mockProcessFilesToUserFiles.mockReturnValue([FILE]) + mockAssertToolFileAccess.mockResolvedValue(null) + mockDownload.mockResolvedValue({ + buffer: Buffer.from('pdf'), + contentType: 'application/pdf', + }) + mockGetJiraCloudId.mockResolvedValue(CLOUD_ID) + fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => [{ id: '1', filename: 'report.pdf', mimeType: 'application/pdf', size: 3 }], + text: async () => '', + }) + vi.stubGlobal('fetch', fetchMock) +}) + +function outgoingUrl(): URL { + expect(fetchMock).toHaveBeenCalledTimes(1) + return new URL(fetchMock.mock.calls[0][0] as string) +} + +describe('POST /api/tools/jira/add-attachment path safety', () => { + /** The exact shape a legitimate call produces, asserted segment by segment. */ + it('builds the documented attachments path for legitimate values', async () => { + const response = await POST(createMockRequest('POST', body())) + expect(response.status).toBe(200) + + const url = outgoingUrl() + expect(url.origin).toBe(ORIGIN) + expect(url.pathname.split('/')).toEqual([ + '', + 'ex', + 'jira', + CLOUD_ID, + 'rest', + 'api', + '3', + 'issue', + 'PROJ-123', + 'attachments', + ]) + expect([...url.searchParams.keys()]).toEqual([]) + }) + + it.each(['..', '.', 'a/../b', '%2e%2e', '..%2f..'])( + 'rejects issueKey=%j with a 400 and never calls Jira', + async (issueKey) => { + const response = await POST(createMockRequest('POST', body({ issueKey }))) + + expect(response.status).toBe(400) + expect((await response.json()).error).toMatch(/issueKey/) + expect(fetchMock).not.toHaveBeenCalled() + } + ) + + it.each(['..', '.', 'a/../b', '%2e%2e', '..%2f..'])( + 'rejects body-supplied cloudId=%j with a 400 and never calls Jira', + async (cloudId) => { + const response = await POST(createMockRequest('POST', body({ cloudId }))) + + expect(response.status).toBe(400) + expect((await response.json()).error).toMatch(/cloudId/) + expect(fetchMock).not.toHaveBeenCalled() + } + ) + + /** cloudId sits earlier in the path, so it is validated first, like the siblings. */ + it('reports cloudId before issueKey when both are hostile', async () => { + const response = await POST(createMockRequest('POST', body({ cloudId: '..', issueKey: '..' }))) + + expect(response.status).toBe(400) + expect((await response.json()).error).toMatch(/cloudId/) + }) + + /** A discovered cloudId is not caller-controlled, but it still shares the guard. */ + it('rejects a discovered cloudId that is a dot segment', async () => { + mockGetJiraCloudId.mockResolvedValue('..') + + const response = await POST(createMockRequest('POST', body({ cloudId: undefined }))) + + expect(response.status).toBe(400) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('still resolves the cloudId from the domain when the body omits it', async () => { + const response = await POST(createMockRequest('POST', body({ cloudId: undefined }))) + + expect(response.status).toBe(200) + expect(mockGetJiraCloudId).toHaveBeenCalledWith('example.atlassian.net', 'inert-token') + expect(outgoingUrl().pathname.split('/')[3]).toBe(CLOUD_ID) + }) +}) diff --git a/apps/sim/app/api/tools/jira/add-attachment/route.ts b/apps/sim/app/api/tools/jira/add-attachment/route.ts index 70218256ffc..ca345404e61 100644 --- a/apps/sim/app/api/tools/jira/add-attachment/route.ts +++ b/apps/sim/app/api/tools/jira/add-attachment/route.ts @@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { jiraAddAttachmentContract } from '@/lib/api/contracts/selectors/jira' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -44,6 +45,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { validatedData.cloudId || (await getJiraCloudId(validatedData.domain, validatedData.accessToken)) + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const issueKeyValidation = validateJiraIssueKey(validatedData.issueKey, 'issueKey') + if (!issueKeyValidation.isValid) { + return NextResponse.json({ error: issueKeyValidation.error }, { status: 400 }) + } + const formData = new FormData() // Every attachment lands in the same multipart body, so the ceiling covers the // set rather than each file on its own. diff --git a/apps/sim/app/api/tools/twilio/get-recording/route.test.ts b/apps/sim/app/api/tools/twilio/get-recording/route.test.ts new file mode 100644 index 00000000000..dbb64aafac0 --- /dev/null +++ b/apps/sim/app/api/tools/twilio/get-recording/route.test.ts @@ -0,0 +1,282 @@ +/** + * @vitest-environment node + * + * Guards the provider URL this route builds from caller-supplied values. + * + * `accountSid` and `recordingSid` are NOT credential-derived — the tool body is + * `{accountSid, authToken, recordingSid}` (`tools/twilio_voice/get_recording.ts`) + * and `recordingSid` is `visibility: 'user-or-llm'`, so prompt injection controls + * it. The contract only enforces `.min(1)`, and `validateUrlWithDNS` pins the + * *host*, not the path. Before the guard, `recordingSid = '../Messages'` resolved + * `/2010-04-01/Accounts/{acct}/Recordings/../Messages.json` down to + * `/2010-04-01/Accounts/{acct}/Messages.json`; the route then follows `data.uri`, + * refetches it with the caller's Basic auth, and returns the body base64-encoded + * as a `file` output — an arbitrary authenticated GET across the caller's Twilio + * account, exfiltrated as an attachment. + * + * Every URL assertion resolves through `new URL(...)` — the same normalization + * `fetch` performs — and compares the full decoded `pathname` segment list. + */ +import { + createMockRequest, + hybridAuthMockFns, + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { MAX_TWILIO_RECORDING_BYTES, POST } from '@/app/api/tools/twilio/get-recording/route' + +const { mockValidateUrlWithDNS, mockSecureFetchWithPinnedIP } = inputValidationMockFns + +const PINNED_IP = '3.89.10.20' + +/** A real Twilio SID: a two-letter prefix followed by 32 hexadecimal digits. */ +const ACCOUNT_SID = `AC${'a'.repeat(32)}` +const RECORDING_SID = 'RE0123456789abcdef0123456789abcdef' + +const baseBody = { + accountSid: ACCOUNT_SID, + authToken: 'auth-token', + recordingSid: RECORDING_SID, +} + +function jsonResponse(body: unknown, ok = true) { + return { + ok, + status: ok ? 200 : 400, + statusText: '', + headers: new Headers(), + body: null, + text: async () => JSON.stringify(body), + json: async () => body, + arrayBuffer: async () => new ArrayBuffer(0), + } +} + +function mediaResponse(bytes: number) { + return { + ok: true, + status: 200, + statusText: '', + headers: new Headers({ 'content-type': 'audio/wav' }), + body: null, + text: async () => '', + json: async () => ({}), + arrayBuffer: async () => new ArrayBuffer(bytes), + } +} + +const recordingPayload = { + sid: RECORDING_SID, + call_sid: 'CA0123456789abcdef0123456789abcdef', + duration: '12', + status: 'completed', + uri: `/2010-04-01/Accounts/${ACCOUNT_SID}/Recordings/${RECORDING_SID}.json`, +} + +beforeEach(() => { + vi.clearAllMocks() + hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + authType: 'internal_jwt', + }) + mockValidateUrlWithDNS.mockResolvedValue({ + isValid: true, + resolvedIP: PINNED_IP, + originalHostname: 'api.twilio.com', + }) +}) + +/** Vectors that must never reach `secureFetchWithPinnedIP` at all. */ +const REJECTED_SIDS = [ + '..', + '.', + ' .. ', + '%2e%2e', + 'a/b', + '../Messages', + '..\\..', + 'RE0123456789abcdef0123456789abcdef/../../Messages', + 'RE0123456789abcdef0123456789abcdef?PageSize=1000', + 'RE0123456789abcdef0123456789abcdef.json', + 'RE0123456789abcdef0123456789abcdefEXTRA', + 'REzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', +] as const + +describe('POST /api/tools/twilio/get-recording — recordingSid guard', () => { + it.each(REJECTED_SIDS)( + 'rejects recordingSid %j with a 400 and issues no request', + async (sid) => { + const response = await POST(createMockRequest('POST', { ...baseBody, recordingSid: sid })) + + expect(response.status).toBe(400) + const data = (await response.json()) as { success: boolean; error: string } + expect(data.success).toBe(false) + expect(data.error).toMatch(/Recording SID/i) + expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + } + ) + + it.each(['..', 'a/b', `AC${'0123456789abcdef'.repeat(2)}/../..`, 'ACnothex'] as const)( + 'rejects accountSid %j with a 400 and issues no request', + async (sid) => { + const response = await POST(createMockRequest('POST', { ...baseBody, accountSid: sid })) + + expect(response.status).toBe(400) + const data = (await response.json()) as { success: boolean; error: string } + expect(data.success).toBe(false) + expect(data.error).toMatch(/Account SID/i) + expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + } + ) +}) + +describe('POST /api/tools/twilio/get-recording — legitimate values', () => { + it('builds byte-identical Twilio URLs for real SIDs', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(jsonResponse(recordingPayload)) + .mockResolvedValueOnce(jsonResponse({ transcriptions: [] })) + .mockResolvedValueOnce(mediaResponse(2048)) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(200) + + const infoUrl = mockSecureFetchWithPinnedIP.mock.calls[0][0] as string + expect(infoUrl).toBe( + `https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Recordings/${RECORDING_SID}.json` + ) + const resolvedInfo = new URL(infoUrl) + expect(resolvedInfo.origin).toBe('https://api.twilio.com') + expect(resolvedInfo.search).toBe('') + expect(resolvedInfo.pathname.split('/').map(decodeURIComponent)).toEqual([ + '', + '2010-04-01', + 'Accounts', + ACCOUNT_SID, + 'Recordings', + `${RECORDING_SID}.json`, + ]) + + const transcriptionUrl = new URL(mockSecureFetchWithPinnedIP.mock.calls[1][0] as string) + expect(transcriptionUrl.pathname.split('/').map(decodeURIComponent)).toEqual([ + '', + '2010-04-01', + 'Accounts', + ACCOUNT_SID, + 'Transcriptions.json', + ]) + expect(transcriptionUrl.searchParams.get('RecordingSid')).toBe(RECORDING_SID) + + const data = (await response.json()) as { success: boolean; output: { file: { size: number } } } + expect(data.success).toBe(true) + expect(data.output.file.size).toBe(2048) + }) + + it('keeps a Twilio-supplied sid inside the query zone via URLSearchParams', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(jsonResponse({ ...recordingPayload, sid: 'RE&PageSize=1000' })) + .mockResolvedValueOnce(jsonResponse({ transcriptions: [] })) + .mockResolvedValueOnce(mediaResponse(16)) + + await POST(createMockRequest('POST', baseBody)) + + const transcriptionUrl = new URL(mockSecureFetchWithPinnedIP.mock.calls[1][0] as string) + expect(transcriptionUrl.searchParams.get('RecordingSid')).toBe('RE&PageSize=1000') + expect(transcriptionUrl.searchParams.get('PageSize')).toBeNull() + expect([...transcriptionUrl.searchParams.keys()]).toEqual(['RecordingSid']) + }) +}) + +describe('POST /api/tools/twilio/get-recording — media size cap', () => { + it('caps the media download at the size the JSON transport can actually carry', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(jsonResponse(recordingPayload)) + .mockResolvedValueOnce(jsonResponse({ transcriptions: [] })) + .mockResolvedValueOnce(mediaResponse(64)) + + await POST(createMockRequest('POST', baseBody)) + + expect(MAX_TWILIO_RECORDING_BYTES).toBeLessThanOrEqual(7.5 * 1024 * 1024) + expect(mockSecureFetchWithPinnedIP.mock.calls[2][2]).toMatchObject({ + maxResponseBytes: MAX_TWILIO_RECORDING_BYTES, + }) + }) +}) + +describe('POST /api/tools/twilio/get-recording — over-limit media', () => { + /** + * The cap is enforced by `secureFetchWithPinnedIP` while the body streams, so + * it surfaces as a rejected promise *inside* the media `try`. A blanket + * `catch` there logged a warning and fell through to the success response + * with `file` simply absent — indistinguishable, to the caller, from a + * recording that has no media yet. An unavailable recording must never look + * like a retrieved one. + */ + it('returns an explicit failure, not success-with-no-file, when the media exceeds the cap', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(jsonResponse(recordingPayload)) + .mockResolvedValueOnce(jsonResponse({ transcriptions: [] })) + .mockRejectedValueOnce( + new PayloadSizeLimitError({ + label: 'response body', + maxBytes: MAX_TWILIO_RECORDING_BYTES, + observedBytes: MAX_TWILIO_RECORDING_BYTES + 1, + }) + ) + + const response = await POST(createMockRequest('POST', baseBody)) + + expect(response.status).toBe(413) + const data = (await response.json()) as { + success: boolean + error?: string + output?: { file?: unknown } + } + expect(data.success).toBe(false) + expect(data.error).toBeTruthy() + expect(data.error).toMatch(new RegExp(String(MAX_TWILIO_RECORDING_BYTES))) + expect(data.output?.file).toBeUndefined() + }) + + it('still degrades to success-without-file for the other media errors that catch exists for', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(jsonResponse(recordingPayload)) + .mockResolvedValueOnce(jsonResponse({ transcriptions: [] })) + .mockRejectedValueOnce(new Error('socket hang up')) + + const response = await POST(createMockRequest('POST', baseBody)) + + expect(response.status).toBe(200) + const data = (await response.json()) as { + success: boolean + output: { file?: unknown; mediaUrl?: string } + } + expect(data.success).toBe(true) + expect(data.output.file).toBeUndefined() + expect(data.output.mediaUrl).toBe( + `https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Recordings/${RECORDING_SID}` + ) + }) + + it('still degrades to success-without-file when media URL validation fails', async () => { + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(jsonResponse(recordingPayload)) + .mockResolvedValueOnce(jsonResponse({ transcriptions: [] })) + mockValidateUrlWithDNS + .mockResolvedValueOnce({ isValid: true, resolvedIP: PINNED_IP }) + .mockResolvedValueOnce({ isValid: true, resolvedIP: PINNED_IP }) + .mockResolvedValueOnce({ isValid: false, error: 'blocked host' }) + + const response = await POST(createMockRequest('POST', baseBody)) + + expect(response.status).toBe(200) + const data = (await response.json()) as { success: boolean; output: { file?: unknown } } + expect(data.success).toBe(true) + expect(data.output.file).toBeUndefined() + }) +}) diff --git a/apps/sim/app/api/tools/twilio/get-recording/route.ts b/apps/sim/app/api/tools/twilio/get-recording/route.ts index ddd0fc9350c..8341b2d6aba 100644 --- a/apps/sim/app/api/tools/twilio/get-recording/route.ts +++ b/apps/sim/app/api/tools/twilio/get-recording/route.ts @@ -9,6 +9,7 @@ import { validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' +import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getExtensionFromMimeType } from '@/lib/uploads/utils/file-utils' @@ -16,6 +17,40 @@ export const dynamic = 'force-dynamic' const logger = createLogger('TwilioGetRecordingAPI') +/** + * Shape of a Twilio resource identifier. + * + * Twilio documents every resource id as a 34-character String Identifier: a + * two-letter prefix followed by 32 hexadecimal digits. No `/`, `\`, dot + * segment, `?`, or `#` is legal in one, so pinning the shape has zero + * false-rejection risk while closing the path zone entirely. + * + * This matters because both `accountSid` and `recordingSid` arrive in the + * request body rather than from a credential, and `recordingSid` is + * `visibility: 'user-or-llm'` on the calling tool. `validateUrlWithDNS` pins + * the *host*, not the path, so an unguarded `recordingSid` of `../Messages` + * re-aimed this route at an arbitrary resource in the caller's account — which + * it then refetched with the caller's Basic auth and returned base64-encoded as + * a `file` output. + */ +const ACCOUNT_SID_PATTERN = /^AC[0-9a-fA-F]{32}$/ +const RECORDING_SID_PATTERN = /^RE[0-9a-fA-F]{32}$/ + +/** + * Ceiling on the downloaded recording media. + * + * This route returns the media base64-encoded inside its JSON body, and the + * executor reads an internal tool response through `readToolResponseBody`, + * which caps at `MAX_TOOL_RESPONSE_BODY_BYTES` (10 MB). Base64 inflates by 4/3, + * so the largest recording that can survive the round trip is ~7.5 MB of raw + * audio. Inheriting `DEFAULT_MAX_RESPONSE_BYTES` (100 MB) meant anything larger + * downloaded and encoded in full — peaking at hundreds of MB of live + * allocation — only for the executor to reject the body afterwards with "Tool + * response size limit exceeded". Capping at the reachable size makes the + * transport limit enforce itself while the bytes are still streaming. + */ +export const MAX_TWILIO_RECORDING_BYTES = 7 * 1024 * 1024 + interface TwilioRecordingResponse { sid?: string call_sid?: string @@ -67,11 +102,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const { accountSid, authToken, recordingSid } = parsed.data.body - if (!accountSid.startsWith('AC')) { + if (!ACCOUNT_SID_PATTERN.test(accountSid)) { return NextResponse.json( { success: false, - error: `Invalid Account SID format. Account SID must start with "AC" (you provided: ${accountSid.substring(0, 2)}...)`, + error: `Invalid Account SID format. Account SID must be "AC" followed by 32 hexadecimal digits (you provided: ${accountSid.substring(0, 2)}...)`, + }, + { status: 400 } + ) + } + + if (!RECORDING_SID_PATTERN.test(recordingSid)) { + return NextResponse.json( + { + success: false, + error: `Invalid Recording SID format. Recording SID must be "RE" followed by 32 hexadecimal digits (you provided: ${recordingSid.substring(0, 2)}...)`, }, { status: 400 } ) @@ -134,7 +179,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { | undefined try { - const transcriptionUrl = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Transcriptions.json?RecordingSid=${data.sid}` + const transcriptionQuery = new URLSearchParams({ RecordingSid: data.sid ?? recordingSid }) + const transcriptionUrl = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Transcriptions.json?${transcriptionQuery}` logger.info(`[${requestId}] Checking for transcriptions`) const transcriptionUrlValidation = await validateUrlWithDNS( @@ -182,6 +228,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { { method: 'GET', headers: { Authorization: `Basic ${twilioAuth}` }, + maxResponseBytes: MAX_TWILIO_RECORDING_BYTES, } ) @@ -202,6 +249,29 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } } } catch (error) { + /** + * The size cap is enforced while the body streams, so it surfaces here + * rather than as a non-ok response. It must not share the fall-through + * below: that path returns `success: true` with `file` absent, which is + * exactly how a recording with no media yet looks. Reporting an + * over-limit recording as a successful retrieval that happened to + * return nothing is worse than reporting nothing at all. + */ + if (isPayloadSizeLimitError(error)) { + logger.warn(`[${requestId}] Twilio recording media exceeds the transportable size cap`, { + recordingSid: data.sid ?? recordingSid, + maxBytes: MAX_TWILIO_RECORDING_BYTES, + }) + const message = `Recording media exceeds the maximum transportable size of ${MAX_TWILIO_RECORDING_BYTES} bytes and cannot be returned inline. Download it directly from ${mediaUrl}.` + return NextResponse.json( + { + success: false, + output: { success: false, error: message, mediaUrl }, + error: message, + }, + { status: 413 } + ) + } logger.warn(`[${requestId}] Failed to download recording media:`, error) } } diff --git a/apps/sim/blocks/blocks/agentmail.ts b/apps/sim/blocks/blocks/agentmail.ts index 3fae72cfe53..d47352e7705 100644 --- a/apps/sim/blocks/blocks/agentmail.ts +++ b/apps/sim/blocks/blocks/agentmail.ts @@ -335,17 +335,26 @@ export const AgentMailBlock: BlockConfig = { }, // Delete Thread fields + /** + * AgentMail's DELETE thread endpoint declares no deletion-mode parameter — its only + * documented inputs are the path ids and the `Authorization` header — and its own + * description reads "Permanently deletes a thread and all of its messages." So the choice + * this id used to render — a dropdown defaulting to "No (move to trash)" — could not + * change the request Sim sends, and named an outcome the endpoint does not document. It is + * now a read-only notice. (AgentMail does have a trash label — `include_trash` filters + * list results, `label_trash_read` is an API-key scope — but nothing in the delete + * endpoint's contract routes a thread there.) Keeping the id claimed means the + * warning appears exactly where the misleading choice used to sit, and no future subBlock + * inherits the orphaned 'true'/'false' value still stored in existing workflows. + */ { id: 'permanent', title: 'Permanent Delete', - type: 'dropdown', - options: [ - { label: 'No (move to trash)', id: 'false' }, - { label: 'Yes (permanent)', id: 'true' }, - ], - value: () => 'false', + type: 'text', + hideFromPreview: true, + defaultValue: + 'Deleting a thread is permanent. AgentMail removes the thread and every message in it immediately, and this cannot be undone.', condition: { field: 'operation', value: 'delete_thread' }, - mode: 'advanced', }, // Forward Message fields @@ -586,10 +595,6 @@ export const AgentMailBlock: BlockConfig = { rest.inboxId = inboxIdParam } - if (operation === 'delete_thread' && permanent !== undefined) { - rest.permanent = permanent === 'true' - } - if (operation === 'reply_message' && replyAll !== undefined) { rest.replyAll = replyAll === 'true' } @@ -665,7 +670,6 @@ export const AgentMailBlock: BlockConfig = { threadId: { type: 'string', description: 'Thread ID' }, addLabels: { type: 'string', description: 'Labels to add to thread (comma-separated)' }, removeLabels: { type: 'string', description: 'Labels to remove from thread (comma-separated)' }, - permanent: { type: 'string', description: 'Whether to permanently delete' }, messageId: { type: 'string', description: 'Message ID' }, draftId: { type: 'string', description: 'Draft ID' }, draftInReplyTo: { type: 'string', description: 'Message ID this draft replies to' }, diff --git a/apps/sim/blocks/blocks/algolia.test.ts b/apps/sim/blocks/blocks/algolia.test.ts new file mode 100644 index 00000000000..7942c0f7cd4 --- /dev/null +++ b/apps/sim/blocks/blocks/algolia.test.ts @@ -0,0 +1,75 @@ +/** + * @vitest-environment node + * + * Guards the Algolia block's pagination coercion. + * + * `page`/`hitsPerPage` leave the canvas as short-input strings, and + * `tools.config.params` coerces them so the tools' declared `type: 'number'` + * holds. The coercion must not manufacture a value: `Number('')` is `0`, so + * trimming a whitespace-only string before parsing turned "the user typed + * spaces" into a request for page 0 / 0 hits per page — and because the block + * loop only drops the exactly-empty string, a whitespace-only field reached it. + * Passing such a value through untouched lets `algolia_list_indices`' + * `paginationValue` reject it by name, which is the single validator this + * repo wants doing that job. + */ +import { describe, expect, it } from 'vitest' +import { AlgoliaBlock } from '@/blocks/blocks/algolia' +import { listIndicesTool } from '@/tools/algolia/list_indices' + +function mapParams(params: Record): Record { + const build = AlgoliaBlock.tools.config?.params + if (!build) throw new Error('algolia tools.config.params is not defined') + return build(params) as Record +} + +const BASE = { operation: 'list_indices', applicationId: 'APPID', apiKey: 'KEY' } + +describe('Algolia block pagination coercion', () => { + it.each([' ', '\t', '\n '])('does not turn a whitespace-only page (%j) into 0', (blank) => { + const result = mapParams({ ...BASE, page: blank }) + expect(result.page).not.toBe(0) + expect(result.page).toBe(blank) + }) + + it.each([' ', '\t', '\n '])( + 'does not turn a whitespace-only hitsPerPage (%j) into 0', + (blank) => { + const result = mapParams({ ...BASE, hitsPerPage: blank }) + expect(result.hitsPerPage).not.toBe(0) + expect(result.hitsPerPage).toBe(blank) + } + ) + + it('lets the tool-side validator reject the whitespace value by name', () => { + const build = listIndicesTool.request.url as (p: Record) => string + expect(() => build(mapParams({ ...BASE, page: ' ' }))).toThrow(/page/) + expect(() => build(mapParams({ ...BASE, hitsPerPage: ' ' }))).toThrow(/hitsPerPage/) + }) + + it('still drops an exactly-empty short-input, as today', () => { + const result = mapParams({ ...BASE, page: '', hitsPerPage: '' }) + expect(Object.hasOwn(result, 'page')).toBe(false) + expect(Object.hasOwn(result, 'hitsPerPage')).toBe(false) + }) + + it('coerces legitimate numeric strings byte-identically to today', () => { + expect(mapParams({ ...BASE, page: '0', hitsPerPage: '100' })).toMatchObject({ + page: 0, + hitsPerPage: 100, + }) + expect(mapParams({ ...BASE, page: ' 3 ', hitsPerPage: ' 25 ' })).toMatchObject({ + page: 3, + hitsPerPage: 25, + }) + expect(mapParams({ ...BASE, page: 2, hitsPerPage: 50 })).toMatchObject({ + page: 2, + hitsPerPage: 50, + }) + }) + + it('still passes a non-numeric value through untouched so the tool names it', () => { + expect(mapParams({ ...BASE, page: 'abc' }).page).toBe('abc') + expect(mapParams({ ...BASE, page: '0&hitsPerPage=1000' }).page).toBe('0&hitsPerPage=1000') + }) +}) diff --git a/apps/sim/blocks/blocks/algolia.ts b/apps/sim/blocks/blocks/algolia.ts index 52941c7d295..e5f89be577d 100644 --- a/apps/sim/blocks/blocks/algolia.ts +++ b/apps/sim/blocks/blocks/algolia.ts @@ -605,6 +605,31 @@ Return ONLY the JSON array.`, return defaultValue } + /** + * Pagination fields are short-inputs, so they leave the canvas as + * strings even though every Algolia tool declares them `type: 'number'`. + * Coercing here — in `params`, which runs during execution after + * variables resolve — keeps a `` reference intact; the + * same call in `tools.config.tool` would run during serialization and + * destroy it. A value that is not numeric is passed through untouched + * so the tool reports the offending parameter by name — including a + * whitespace-only string, which the surrounding loop does not drop (it + * drops only the exactly-empty one) and which `Number('')` would + * otherwise turn into a real request for page 0 / 0 hits per page, + * slipping past `paginationValue` as a legitimate number. + */ + const toNumber = (value: unknown) => { + if (typeof value === 'number') return value + if (typeof value !== 'string') return value + const trimmed = value.trim() + if (trimmed === '') return value + const parsed = Number(trimmed) + return Number.isFinite(parsed) ? parsed : value + } + + if (result.page !== undefined) result.page = toNumber(result.page) + if (result.hitsPerPage !== undefined) result.hitsPerPage = toNumber(result.hitsPerPage) + if (operation === 'partial_update_record') { result.createIfNotExists = toBool(result.createIfNotExists, true) } @@ -636,8 +661,8 @@ Return ONLY the JSON array.`, operation: { type: 'string', description: 'Operation to perform' }, indexName: { type: 'string', description: 'Algolia index name' }, query: { type: 'string', description: 'Search query' }, - hitsPerPage: { type: 'string', description: 'Number of hits per page' }, - page: { type: 'string', description: 'Page number' }, + hitsPerPage: { type: 'number', description: 'Number of hits per page' }, + page: { type: 'number', description: 'Page number' }, filters: { type: 'string', description: 'Algolia filter string' }, attributesToRetrieve: { type: 'string', description: 'Attributes to retrieve' }, facets: { type: 'string', description: 'Comma-separated facet attribute names to count' }, diff --git a/apps/sim/blocks/blocks/apify.ts b/apps/sim/blocks/blocks/apify.ts index 7196c0a9b8f..57a770e4904 100644 --- a/apps/sim/blocks/blocks/apify.ts +++ b/apps/sim/blocks/blocks/apify.ts @@ -1,11 +1,14 @@ import { ApifyIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' -import type { RunActorResult } from '@/tools/apify/types' +import { APIFY_SYNC_TRANSPORT_TIMEOUT_MS, type RunActorResult } from '@/tools/apify/types' const RUN_OPERATIONS = ['apify_run_actor_sync', 'apify_run_actor_async'] const RUN_OR_TASK_OPERATIONS = [...RUN_OPERATIONS, 'apify_run_task'] +/** Operations that call a `run-sync-get-dataset-items` endpoint and inherit its 300s/408 contract. */ +const SYNC_RUN_OPERATIONS = new Set(['apify_run_actor_sync', 'apify_run_task']) + export const ApifyBlock: BlockConfig = { type: 'apify', name: 'Apify', @@ -231,7 +234,25 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, if (rest.build) result.build = rest.build if (rest.fields) result.fields = rest.fields if (rest.memory) result.memory = Number(rest.memory) - if (rest.timeout) result.timeout = Number(rest.timeout) + /** + * `timeout` is reserved by the tool request transport as the outbound fetch + * deadline in milliseconds, so the seconds-valued subBlock is remapped onto a + * tool-specific parameter and never forwarded as-is. + * + * The sync endpoints then get a deliberate transport deadline of their own: + * without one they ride the transport's bare 300000ms fallback, which is the + * same number as Apify's documented 300s sync cap, so a boundary run races a + * structured 408 against a generic transport timeout. See + * `APIFY_SYNC_TRANSPORT_TIMEOUT_MS`. + */ + result.timeout = SYNC_RUN_OPERATIONS.has(operation) + ? APIFY_SYNC_TRANSPORT_TIMEOUT_MS + : undefined + if (rest.timeout != null && rest.timeout !== '') { + const timeoutSeconds = Number(rest.timeout) + if (operation === 'apify_run_task') result.taskTimeout = timeoutSeconds + else result.actorTimeout = timeoutSeconds + } if (rest.waitForFinish) result.waitForFinish = Number(rest.waitForFinish) if (rest.itemLimit) result.itemLimit = Number(rest.itemLimit) if (rest.offset !== undefined && rest.offset !== null && rest.offset !== '') diff --git a/apps/sim/blocks/blocks/daytona.ts b/apps/sim/blocks/blocks/daytona.ts index c641c864ba5..9ff950a2096 100644 --- a/apps/sim/blocks/blocks/daytona.ts +++ b/apps/sim/blocks/blocks/daytona.ts @@ -285,7 +285,7 @@ export const DaytonaBlock: BlockConfig = { id: 'timeout', title: 'Timeout (seconds)', type: 'short-input', - placeholder: 'Timeout in seconds (defaults to 10)', + placeholder: 'Timeout in seconds (0 disables the server-side limit)', mode: 'advanced', condition: { field: 'operation', value: ['execute_command', 'run_code'] }, }, @@ -455,6 +455,13 @@ export const DaytonaBlock: BlockConfig = { const baseParams: Record = { apiKey } + /** + * `timeout` is reserved by the tool request transport as the outbound fetch + * deadline in milliseconds, so the seconds-valued subBlock is remapped onto a + * tool-specific parameter and cleared here to keep it off the transport. + */ + baseParams.timeout = undefined + if (SANDBOX_SCOPED_OPERATIONS.includes(operation)) { baseParams.sandboxId = rest.sandboxId } @@ -488,7 +495,7 @@ export const DaytonaBlock: BlockConfig = { baseParams.language = rest.language if (rest.runEnv) baseParams.env = rest.runEnv if (rest.timeout !== undefined && rest.timeout !== '') { - baseParams.timeout = Number(rest.timeout) + baseParams.runTimeout = Number(rest.timeout) } break case 'execute_command': @@ -496,7 +503,7 @@ export const DaytonaBlock: BlockConfig = { if (rest.cwd) baseParams.cwd = rest.cwd if (rest.runEnv) baseParams.env = rest.runEnv if (rest.timeout !== undefined && rest.timeout !== '') { - baseParams.timeout = Number(rest.timeout) + baseParams.commandTimeout = Number(rest.timeout) } break case 'upload_file': { @@ -593,7 +600,7 @@ export const DaytonaBlock: BlockConfig = { }, result: { type: 'string', - description: 'Combined stdout/stderr output (execute command and run code operations)', + description: 'Standard output (execute command and run code operations)', }, artifacts: { type: 'json', description: 'Run artifacts such as charts (run code operation)' }, uploadedPath: { diff --git a/apps/sim/blocks/blocks/elasticsearch.ts b/apps/sim/blocks/blocks/elasticsearch.ts index 24c1acdba5f..45080a23527 100644 --- a/apps/sim/blocks/blocks/elasticsearch.ts +++ b/apps/sim/blocks/blocks/elasticsearch.ts @@ -2,6 +2,7 @@ import { ElasticsearchIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import type { ElasticsearchResponse } from '@/tools/elasticsearch/types' +import { normalizeEsDuration, optionalNumber } from '@/tools/elasticsearch/utils' export const ElasticsearchBlock: BlockConfig = { type: 'elasticsearch', @@ -186,7 +187,28 @@ export const ElasticsearchBlock: BlockConfig = { title: 'Index Name', type: 'short-input', placeholder: 'my-index', - required: true, + /** + * Required for every operation that addresses an index, but *not* for + * `_bulk`. Bulk legitimately runs against `/_bulk` with a per-action + * `_index` — the `operations` placeholder below shows exactly that — so + * `bulk.ts` declares `index` optional and its URL builder branches on + * the absence. The field still renders for bulk, where it supplies the + * default target for actions that omit `_index`. + */ + required: { + field: 'operation', + value: [ + 'elasticsearch_search', + 'elasticsearch_index_document', + 'elasticsearch_get_document', + 'elasticsearch_update_document', + 'elasticsearch_delete_document', + 'elasticsearch_count', + 'elasticsearch_create_index', + 'elasticsearch_delete_index', + 'elasticsearch_get_index', + ], + }, condition: { field: 'operation', value: [ @@ -324,6 +346,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, // Search from (offset) { id: 'from', + mode: 'advanced', title: 'Offset', type: 'short-input', placeholder: '0', @@ -333,6 +356,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, // Sort { id: 'sort', + mode: 'advanced', title: 'Sort', type: 'code', placeholder: '[{ "field": { "order": "asc" } }]', @@ -354,6 +378,7 @@ Return ONLY valid JSON array - no explanations, no markdown code blocks.`, // Source includes { id: 'sourceIncludes', + mode: 'advanced', title: 'Fields to Include', type: 'short-input', placeholder: 'field1, field2 (comma-separated)', @@ -366,6 +391,7 @@ Return ONLY valid JSON array - no explanations, no markdown code blocks.`, // Source excludes { id: 'sourceExcludes', + mode: 'advanced', title: 'Fields to Exclude', type: 'short-input', placeholder: 'field1, field2 (comma-separated)', @@ -403,6 +429,7 @@ Return ONLY the NDJSON content - no explanations, no markdown code blocks.`, // Index settings { id: 'settings', + mode: 'advanced', title: 'Index Settings', type: 'code', placeholder: '{ "number_of_shards": 1, "number_of_replicas": 1 }', @@ -449,6 +476,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, // Refresh option { id: 'refresh', + mode: 'advanced', title: 'Refresh', type: 'dropdown', options: [ @@ -471,6 +499,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, // Cluster health wait for status { id: 'waitForStatus', + mode: 'advanced', title: 'Wait for Status', type: 'dropdown', options: [ @@ -486,15 +515,17 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, // Cluster health timeout { id: 'timeout', - title: 'Timeout (seconds)', + mode: 'advanced', + title: 'Wait Timeout', type: 'short-input', - placeholder: '30', + placeholder: '30s', condition: { field: 'operation', value: 'elasticsearch_cluster_health' }, }, // Retry on conflict { id: 'retryOnConflict', + mode: 'advanced', title: 'Retry on Conflict', type: 'short-input', placeholder: '3', @@ -524,13 +555,41 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, return params.operation || 'elasticsearch_search' }, params: (params) => { - const result: Record = {} - if (params.size) result.size = Number(params.size) - if (params.from) result.from = Number(params.from) - if (params.retryOnConflict) result.retryOnConflict = Number(params.retryOnConflict) - if (params.timeout && typeof params.timeout === 'string') { - result.timeout = params.timeout.endsWith('s') ? params.timeout : `${params.timeout}s` + /** + * `result.timeout` is unset unconditionally and on purpose. + * + * `tools/request-transport.ts` reads `params.timeout` as the transport + * deadline in **milliseconds**, while Elasticsearch's `timeout` is a + * duration string. `generic-handler.ts` merges `{ ...inputs, + * ...transformedParams }`, so a raw `timeout` left on `inputs` would + * still reach the transport — a model emitting "30" would buy a + * 30-millisecond deadline. The subBlock keeps the id `timeout` so no + * saved workflow state is orphaned; the value is forwarded to the tool + * under the non-reserved name `esTimeout`. + */ + const result: Record = { timeout: undefined } + + const size = optionalNumber(params.size, 'size') + if (size !== undefined) result.size = size + + const from = optionalNumber(params.from, 'from') + if (from !== undefined) result.from = from + + const retryOnConflict = optionalNumber(params.retryOnConflict, 'retryOnConflict') + if (retryOnConflict !== undefined) result.retryOnConflict = retryOnConflict + + /** + * The unit coercion itself lives in `normalizeEsDuration` on the tool, + * not here: Copilot calls `executeAppTool` directly and never runs this + * mapping, so a normalization owned by the block would apply to one + * calling surface only. This call keeps the subBlock's raw value from + * being forwarded under a different name than the tool expects. + */ + const esTimeout = normalizeEsDuration(params.timeout) + if (esTimeout) { + result.esTimeout = esTimeout } + return result }, }, @@ -559,7 +618,11 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, mappings: { type: 'string', description: 'Index mappings as JSON' }, refresh: { type: 'string', description: 'Refresh policy' }, waitForStatus: { type: 'string', description: 'Wait for cluster status' }, - timeout: { type: 'string', description: 'Timeout for wait operations' }, + timeout: { + type: 'string', + description: + 'How long cluster health waits for the requested status, as a duration string (30s, 1m). A bare number is read as seconds.', + }, retryOnConflict: { type: 'number', description: 'Retry attempts on conflict' }, }, @@ -568,14 +631,21 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, hits: { type: 'json', description: 'Search results' }, took: { type: 'number', description: 'Time taken in milliseconds' }, timed_out: { type: 'boolean', description: 'Whether the operation timed out' }, - aggregations: { type: 'json', description: 'Aggregation results' }, // Document outputs _index: { type: 'string', description: 'Index name' }, _id: { type: 'string', description: 'Document ID' }, _version: { type: 'number', description: 'Document version' }, _source: { type: 'json', description: 'Document content' }, - result: { type: 'string', description: 'Operation result' }, - found: { type: 'boolean', description: 'Whether document was found' }, + result: { + type: 'string', + description: + 'Operation result (created, updated, deleted, or noop). On the single-document operations a missing document fails the call with a 404 rather than returning "not_found"; Bulk Operations instead reports a per-item "not_found" inside a successful response, under items.', + }, + found: { + type: 'boolean', + description: + 'Always true on Get Document — a missing document fails the call with a 404 rather than returning found: false.', + }, // Bulk outputs errors: { type: 'boolean', description: 'Whether any errors occurred' }, items: { type: 'json', description: 'Bulk operation results' }, @@ -583,11 +653,23 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, count: { type: 'number', description: 'Document count' }, // Index outputs acknowledged: { type: 'boolean', description: 'Whether operation was acknowledged' }, + index: { type: 'string', description: 'Index name' }, + matchedCount: { + type: 'number', + description: 'How many indices a Get Index target matched', + }, + aliases: { type: 'json', description: 'Aliases defined on the index' }, + mappings: { type: 'json', description: 'Field mappings for the index' }, + settings: { type: 'json', description: 'Index settings' }, // Cluster outputs cluster_name: { type: 'string', description: 'Cluster name' }, status: { type: 'string', description: 'Cluster health status' }, number_of_nodes: { type: 'number', description: 'Number of nodes' }, - indices: { type: 'json', description: 'Index statistics' }, + indices: { + type: 'json', + description: + 'Every index the operation returned: the array of index rows from List Indices, the per-index map from Get Index (each entry carrying its aliases, mappings, settings, and any data_stream or lifecycle), or the cluster index statistics from Cluster Stats.', + }, nodes: { type: 'json', description: 'Node statistics' }, }, } diff --git a/apps/sim/blocks/blocks/enrow.ts b/apps/sim/blocks/blocks/enrow.ts index d55d362bffe..4ebedc8b013 100644 --- a/apps/sim/blocks/blocks/enrow.ts +++ b/apps/sim/blocks/blocks/enrow.ts @@ -138,4 +138,71 @@ export const EnrowBlock: BlockConfig = { export const EnrowBlockMeta = { tags: ['enrichment', 'sales-engagement'], url: 'https://enrow.io', + templates: [ + { + icon: EnrowIcon, + title: 'Enrow email finder', + prompt: + 'Build a workflow that reads a prospect full name and company domain from a table, finds the verified work email with Enrow, and writes the address and its qualification back to the row.', + modules: ['tables', 'agent', 'workflows'], + category: 'sales', + tags: ['sales', 'research'], + }, + { + icon: EnrowIcon, + title: 'Enrow list cleaner', + prompt: + 'Create a workflow that runs every address in an email list through Enrow verification and writes only the deliverable ones into a clean sending table.', + modules: ['tables', 'agent', 'workflows'], + category: 'sales', + tags: ['sales', 'automation'], + }, + { + icon: EnrowIcon, + title: 'Enrow form-signup verifier', + prompt: + 'Build a workflow that verifies each new signup email with Enrow when a form is submitted, and routes undeliverable addresses to a review queue instead of the CRM.', + modules: ['agent', 'workflows'], + category: 'sales', + tags: ['sales', 'automation'], + }, + { + icon: EnrowIcon, + title: 'Enrow CRM gap-filler', + prompt: + 'Create a scheduled workflow that finds HubSpot contacts with no email, looks each one up in Enrow from the name and company domain, and updates the contact record when a valid address is found.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'sales', + tags: ['sales', 'crm'], + alsoIntegrations: ['hubspot'], + }, + { + icon: EnrowIcon, + title: 'Enrow bounce guard', + prompt: + "Build a scheduled workflow that re-verifies the contacts queued for this week's outbound send with Enrow and removes any address that no longer qualifies as valid.", + modules: ['scheduled', 'tables', 'agent', 'workflows'], + category: 'sales', + tags: ['sales', 'automation'], + }, + { + icon: EnrowIcon, + title: 'Enrow inbound-lead router', + prompt: + 'Create a workflow that on a new inbound lead finds the work email with Enrow, verifies it, and posts the qualified contact to the sales channel in Slack.', + modules: ['agent', 'workflows'], + category: 'sales', + tags: ['sales', 'communication'], + alsoIntegrations: ['slack'], + }, + { + icon: EnrowIcon, + title: 'Enrow find-and-verify pipeline', + prompt: + 'Build a workflow that takes a list of names and companies, finds each work email with Enrow, verifies the result, and writes a ready-to-contact table with the qualification for every row.', + modules: ['tables', 'agent', 'workflows'], + category: 'sales', + tags: ['sales', 'research', 'enrichment'], + }, + ], } as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/firecrawl.ts b/apps/sim/blocks/blocks/firecrawl.ts index e7a93b22314..494348c3a85 100644 --- a/apps/sim/blocks/blocks/firecrawl.ts +++ b/apps/sim/blocks/blocks/firecrawl.ts @@ -7,6 +7,24 @@ import type { FirecrawlResponse } from '@/tools/firecrawl/types' /** The document being parsed, whether it was uploaded or passed in by reference. */ const DOCUMENT_FIELD = ['fileUpload', 'fileReference'] as const +/** + * Normalize a `multiSelect` dropdown value into the string array the tool wants. + * + * The control persists either an array of option ids or a comma-joined string + * depending on how the value was written, and an untouched field is `null`. + * Returns `undefined` when nothing is selected so the caller can omit the key + * and let Firecrawl apply its own default. + */ +function toSelectedIds(value: unknown): string[] | undefined { + const entries = Array.isArray(value) ? value : [value] + const ids = entries + .filter((entry): entry is string => typeof entry === 'string') + .flatMap((entry) => entry.split(',')) + .map((id) => id.trim()) + .filter((id) => id.length > 0) + return ids.length > 0 ? ids : undefined +} + export const FirecrawlBlock: BlockConfig = { type: 'firecrawl', name: 'Firecrawl', @@ -300,7 +318,7 @@ Example 2 - Product Data: title: 'Ignore Invalid URLs', type: 'switch', mode: 'advanced', - condition: { field: 'operation', value: 'batch_scrape' }, + condition: { field: 'operation', value: ['batch_scrape', 'search'] }, }, { id: 'waitFor', @@ -429,6 +447,68 @@ Example 2 - Product Data: }, required: true, }, + { + id: 'sources', + title: 'Sources', + type: 'dropdown', + multiSelect: true, + options: [ + { label: 'Web', id: 'web' }, + { label: 'News', id: 'news' }, + { label: 'Images', id: 'images' }, + ], + placeholder: 'Web', + description: + 'Which result sets to search. Each one is returned as its own array — web, news, images.', + condition: { + field: 'operation', + value: 'search', + }, + }, + { + id: 'categories', + title: 'Categories', + type: 'dropdown', + multiSelect: true, + options: [ + { label: 'GitHub', id: 'github' }, + { label: 'Research', id: 'research' }, + { label: 'PDF', id: 'pdf' }, + { label: 'Developer', id: 'developer' }, + ], + placeholder: 'All categories', + description: + 'Restrict web results to these categories. Developer cannot be combined with the others.', + mode: 'advanced', + condition: { + field: 'operation', + value: 'search', + }, + }, + { + id: 'location', + title: 'Location', + type: 'short-input', + placeholder: 'Germany', + description: 'Where to search from. Set Country alongside it for best results.', + mode: 'advanced', + condition: { + field: 'operation', + value: 'search', + }, + }, + { + id: 'country', + title: 'Country', + type: 'short-input', + placeholder: 'US', + description: 'ISO country code for geo-targeting, e.g. US, DE, JP.', + mode: 'advanced', + condition: { + field: 'operation', + value: 'search', + }, + }, { id: 'apiKey', title: 'API Key', @@ -527,7 +607,20 @@ Example 2 - Product Data: jobId, } = params - const result: Record = { apiKey } + /** + * `timeout` is reserved by the tool request transport as the outbound fetch + * deadline. On the external branch it is passed through bare, arming Sim's + * abort at the exact budget Firecrawl was given, on a clock that starts + * earlier — so Sim always wins and the caller loses Firecrawl's structured + * 408. Clear it for every operation; the external tools carry the value as + * `firecrawlTimeout` instead, while `parse` posts to an internal route that + * wants the deadline propagated and sets it back below. + * + * Clearing it here rather than per-case also covers the operations whose + * `timeout` subBlock is hidden by `condition`: the saved value survives an + * operation switch and would otherwise still reach the transport. + */ + const result: Record = { apiKey, timeout: undefined } switch (operation) { case 'scrape': @@ -544,17 +637,27 @@ Example 2 - Product Data: } } } - if (timeout) result.timeout = Number.parseInt(timeout) + if (timeout) result.firecrawlTimeout = Number.parseInt(timeout) if (waitFor) result.waitFor = Number.parseInt(waitFor) if (onlyMainContent != null) result.onlyMainContent = onlyMainContent if (mobile != null) result.mobile = mobile break - case 'search': + case 'search': { if (query) result.query = query - if (timeout) result.timeout = Number.parseInt(timeout) + if (timeout) result.firecrawlTimeout = Number.parseInt(timeout) if (limit) result.limit = Number.parseInt(limit) + if (params.ignoreInvalidURLs != null) { + result.ignoreInvalidURLs = params.ignoreInvalidURLs + } + const sources = toSelectedIds(params.sources) + if (sources) result.sources = sources + const categories = toSelectedIds(params.categories) + if (categories) result.categories = categories + if (params.location) result.location = params.location + if (params.country) result.country = params.country break + } case 'crawl': if (url) result.url = url @@ -730,6 +833,13 @@ Example 2 - Product Data: urls: { type: 'json', description: 'Array of URLs for extraction or batch scraping' }, jobId: { type: 'string', description: 'Job ID for status/cancel operations' }, query: { type: 'string', description: 'Search query terms' }, + sources: { type: 'json', description: 'Search result sources (web, news, images)' }, + categories: { + type: 'json', + description: 'Search result categories to restrict web results to', + }, + location: { type: 'string', description: 'Location to search from' }, + country: { type: 'string', description: 'ISO country code for geo-targeting search results' }, prompt: { type: 'string', description: 'Extraction prompt' }, limit: { type: 'string', description: 'Result/page limit' }, formats: { type: 'json', description: 'Output formats array' }, @@ -773,6 +883,8 @@ Example 2 - Product Data: removeBase64Images: { type: 'boolean', description: 'Remove base64 images, keep alt text' }, blockAds: { type: 'boolean', description: 'Block ads and popups during parsing' }, proxy: { type: 'string', description: 'Proxy mode (basic or auto)' }, + maxConcurrency: { type: 'string', description: 'Maximum number of concurrent scrapes' }, + ignoreInvalidURLs: { type: 'boolean', description: 'Skip URLs that are invalid for Firecrawl' }, zeroDataRetention: { type: 'boolean', description: 'Enable zero data retention' }, }, outputs: { @@ -792,7 +904,11 @@ Example 2 - Product Data: invalidURLs: { type: 'json', description: 'URLs skipped because they were invalid' }, // Map output success: { type: 'boolean', description: 'Operation success status' }, - links: { type: 'json', description: 'Discovered URLs array' }, + links: { + type: 'json', + description: + 'Discovered links, each an object with `url` and optional `title`/`description`. Reference `[i].url` for the address — the entry itself is no longer a bare URL string.', + }, // Extract output sources: { type: 'json', description: 'Data sources array' }, tokensUsed: { type: 'number', description: 'Tokens consumed by the extract job' }, diff --git a/apps/sim/blocks/blocks/google_contacts.ts b/apps/sim/blocks/blocks/google_contacts.ts index 05f7d897d37..7ad0ce19ae5 100644 --- a/apps/sim/blocks/blocks/google_contacts.ts +++ b/apps/sim/blocks/blocks/google_contacts.ts @@ -5,6 +5,27 @@ import { AuthMode, IntegrationType } from '@/blocks/types' import { SERVICE_ACCOUNT_SUBBLOCKS } from '@/blocks/utils' import type { GoogleContactsResponse } from '@/tools/google_contacts/types' +/** + * Per-operation `pageSize` ceilings. `people.connections.list` accepts 1–1000 + * (default 100), while `people:searchContacts` silently caps anything above 30 + * to 30 (default 10). One shared subBlock feeds both, so the value is clamped + * to the ceiling of whichever operation is selected rather than sending a + * number the target operation would quietly reduce. + * + * `0` is not clamped, it is dropped. Both discovery descriptions give it a + * meaning of its own — list: "Defaults to 100 if not set or set to 0"; + * search: "Defaults to 10 if field is not set, or set to 0" — so clamping it + * up to the minimum of `1` turned a request for the default page into a + * request for a single contact. Omitting the field reproduces "not set", which + * is the same default Google documents for `0`. + * @see https://developers.google.com/people/api/rest/v1/people.connections/list + * @see https://developers.google.com/people/api/rest/v1/people/searchContacts + */ +const PAGE_SIZE_MAX_BY_OPERATION: Record = { + list: 1000, + search: 30, +} + export const GoogleContactsBlock: BlockConfig = { type: 'google_contacts', name: 'Google Contacts', @@ -193,7 +214,8 @@ export const GoogleContactsBlock: BlockConfig = { id: 'pageSize', title: 'Page Size', type: 'short-input', - placeholder: '100', + placeholder: 'Number of results', + tooltip: 'List accepts 1–1000 (default 100). Search accepts 1–30 (default 10).', condition: { field: 'operation', value: ['list', 'search'] }, mode: 'advanced', }, @@ -253,9 +275,13 @@ export const GoogleContactsBlock: BlockConfig = { const processedParams: Record = { ...rest } - // Convert pageSize to number if provided - if (processedParams.pageSize && typeof processedParams.pageSize === 'string') { - processedParams.pageSize = Number.parseInt(processedParams.pageSize, 10) + const pageSizeMax = PAGE_SIZE_MAX_BY_OPERATION[operation] + if (pageSizeMax !== undefined && processedParams.pageSize !== undefined) { + const parsed = Number.parseInt(String(processedParams.pageSize), 10) + processedParams.pageSize = + Number.isNaN(parsed) || parsed === 0 + ? undefined + : Math.min(Math.max(parsed, 1), pageSizeMax) } return { diff --git a/apps/sim/blocks/blocks/google_vault.ts b/apps/sim/blocks/blocks/google_vault.ts index c5eb6608df2..9360177c010 100644 --- a/apps/sim/blocks/blocks/google_vault.ts +++ b/apps/sim/blocks/blocks/google_vault.ts @@ -104,7 +104,7 @@ export const GoogleVaultBlock: BlockConfig = { ], delete_matters: [ { - text: 'Permanently delete matter', + text: 'Delete matter', field: 'matterId', core: true, }, @@ -712,10 +712,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, id: 'role', title: 'Role', type: 'dropdown', - options: [ - { id: 'COLLABORATOR', label: 'Collaborator' }, - { id: 'OWNER', label: 'Owner' }, - ], + options: [{ id: 'COLLABORATOR', label: 'Collaborator' }], condition: { field: 'operation', value: 'add_matters_permissions' }, required: true, value: () => 'COLLABORATOR', @@ -941,7 +938,10 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, // Matter collaborator inputs accountId: { type: 'string', description: 'Admin SDK account ID for collaborator management' }, - role: { type: 'string', description: 'Matter permission role (COLLABORATOR or OWNER)' }, + role: { + type: 'string', + description: 'Matter permission role to grant. The API accepts COLLABORATOR only', + }, sendEmails: { type: 'boolean', description: 'Send a notification email to the added account' }, ccMe: { type: 'boolean', description: 'CC the requestor on the notification email' }, @@ -1135,9 +1135,9 @@ export const GoogleVaultBlockMeta = { { name: 'close-out-matter', description: - 'Wind down a resolved matter by closing it, and permanently delete it once retention requirements are satisfied.', + 'Wind down a resolved matter by closing it, and delete it once retention requirements are satisfied.', content: - '# Close Out Matter\n\nRetire a matter once the underlying investigation or litigation is resolved.\n\n## Steps\n1. List the holds on the matter and confirm none still need to be preserved; delete any holds that are no longer required.\n2. Close the matter.\n3. If the matter should be permanently removed and your retention policy allows it, delete the closed matter (reopen or undelete if this was done in error).\n\n## Output\nReturn the matterId, its final state, and which holds (if any) were deleted before close-out.', + '# Close Out Matter\n\nRetire a matter once the underlying investigation or litigation is resolved.\n\n## Steps\n1. List the holds on the matter and confirm none still need to be preserved; delete any holds that are no longer required.\n2. Close the matter.\n3. If the matter should be removed and your retention policy allows it, delete the closed matter. Deleting is reversible: the matter moves to the DELETED state and can be restored with undelete (or reopened) if this was done in error.\n\n## Output\nReturn the matterId, its final state, and which holds (if any) were deleted before close-out.', }, { name: 'manage-hold-scope', diff --git a/apps/sim/blocks/blocks/incidentio.ts b/apps/sim/blocks/blocks/incidentio.ts index eaf039b83cc..224cc5ed528 100644 --- a/apps/sim/blocks/blocks/incidentio.ts +++ b/apps/sim/blocks/blocks/incidentio.ts @@ -701,8 +701,6 @@ Return ONLY the title - no explanations.`, options: [ { label: 'Newly Created', id: 'newly_created' }, { label: 'Newly Created and Active', id: 'newly_created_and_active' }, - { label: 'Active', id: 'active' }, - { label: 'All', id: 'all' }, ], value: () => 'newly_created', condition: { diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index 44127da9104..a91be3585d7 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -1412,15 +1412,28 @@ Return ONLY the comment text - no explanations.`, outputs: { // Common outputs across all Jira operations ts: { type: 'string', description: 'Timestamp of the operation' }, + success: { type: 'boolean', description: 'Whether the operation succeeded' }, - // jira_retrieve (read) outputs + // jira_retrieve (read) outputs — the transformed issue is spread at the top + // level, so every field below sits alongside the raw `issue` object issueKey: { type: 'string', description: 'Issue key (e.g., PROJ-123)' }, summary: { type: 'string', description: 'Issue summary/title' }, description: { type: 'string', description: 'Issue description content' }, created: { type: 'string', description: 'Issue creation date' }, updated: { type: 'string', description: 'Issue last update date' }, - status: { type: 'string', description: 'Issue status name' }, - assignee: { type: 'string', description: 'Issue assignee display name or account ID' }, + status: { + type: 'json', + description: 'Issue status object with id, name, description, and statusCategory', + }, + statusName: { type: 'string', description: 'Issue status name (e.g., Open, In Progress)' }, + assignee: { + type: 'json', + description: 'Assigned user object with accountId, displayName, and emailAddress', + }, + assigneeName: { + type: 'string', + description: 'Assignee display name or account ID', + }, // jira_write (create) outputs url: { type: 'string', description: 'URL to the created/accessed issue' }, @@ -1447,8 +1460,11 @@ Return ONLY the comment text - no explanations.`, // jira_add_comment, jira_update_comment outputs commentId: { type: 'string', description: 'Comment ID' }, - commentBody: { type: 'string', description: 'Comment text content' }, - author: { type: 'string', description: 'Comment author display name' }, + body: { type: 'string', description: 'Comment text content' }, + author: { + type: 'json', + description: 'Comment author object with accountId, displayName, and emailAddress', + }, // jira_get_attachments outputs attachments: { @@ -1478,7 +1494,11 @@ Return ONLY the comment text - no explanations.`, // jira_transition_issue outputs transitionId: { type: 'string', description: 'Applied transition ID' }, - newStatus: { type: 'string', description: 'New status after transition' }, + transitionName: { type: 'string', description: 'Applied transition name' }, + toStatus: { + type: 'json', + description: 'Target status after transition, with id and name', + }, // jira_create_issue_link outputs linkId: { type: 'string', description: 'Created link ID' }, diff --git a/apps/sim/blocks/blocks/langsmith.ts b/apps/sim/blocks/blocks/langsmith.ts index d4015c46563..59aa74e3750 100644 --- a/apps/sim/blocks/blocks/langsmith.ts +++ b/apps/sim/blocks/blocks/langsmith.ts @@ -177,7 +177,7 @@ export const LangsmithBlock: BlockConfig = { id: 'session_id', title: 'Session ID', type: 'short-input', - placeholder: 'Session identifier', + placeholder: 'Tracing project (session) UUID, e.g. 018e4c7e-a9fb-7ef0-a5b6-6ea3a82e9327', condition: { field: 'operation', value: 'langsmith_create_run' }, mode: 'advanced', }, @@ -251,6 +251,24 @@ Required: id (existing run UUID), name, run_type ("tool"|"chain"|"llm"|"retrieve Common patch fields: outputs, end_time, status, error`, }, }, + { + /** + * The feedback path's own Session ID field. + * + * Deliberately not `required`. The endpoint's prose calls `session_id` required, but + * `FeedbackCreateSchema` lists only `key` in its `required` array and LangSmith's own SDK + * posts feedback without a `session_id`, so enforcing it here would reject workflows the + * API still accepts. It gets its own field — rather than reusing the create-run path's + * `session_id`, which sits behind the advanced toggle — because `mode` is static per + * subBlock; the params mapper falls back to the old shared `session_id` so blocks that + * already stored a value under it keep working. + */ + id: 'feedback_session_id', + title: 'Session ID', + type: 'short-input', + placeholder: 'Tracing project (session) UUID, e.g. 018e4c7e-a9fb-7ef0-a5b6-6ea3a82e9327', + condition: { field: 'operation', value: 'langsmith_create_feedback' }, + }, { id: 'key', title: 'Feedback Key', @@ -400,6 +418,7 @@ Common patch fields: outputs, end_time, status, error`, apiKey: params.apiKey, runId: params.runId, key: params.key, + sessionId: params.feedback_session_id || params.session_id, score: parseScore(params.score), value: params.value, comment: params.comment, @@ -449,7 +468,15 @@ Common patch fields: outputs, end_time, status, error`, tags: { type: 'json', description: 'Tags array' }, parent_run_id: { type: 'string', description: 'Parent run ID' }, trace_id: { type: 'string', description: 'Trace ID' }, - session_id: { type: 'string', description: 'Session ID' }, + session_id: { + type: 'string', + description: 'UUID of the tracing project (session) the run belongs to', + }, + feedback_session_id: { + type: 'string', + description: + 'UUID of the tracing project (session) the feedback belongs to. LangSmith documents it as required and its SDK warns (or errors, on SmithDB-only deployments) without it. Left optional here so blocks saved against deployments that still resolve the run server-side keep working.', + }, session_name: { type: 'string', description: 'Session name' }, status: { type: 'string', description: 'Run status' }, error: { type: 'string', description: 'Error message' }, @@ -459,7 +486,11 @@ Common patch fields: outputs, end_time, status, error`, patch: { type: 'json', description: 'Runs to update in batch' }, key: { type: 'string', description: 'Feedback metric name' }, score: { type: 'string', description: 'Numeric score for the feedback metric' }, - value: { type: 'string', description: 'Categorical value for the feedback metric' }, + value: { + type: 'string', + description: + 'Categorical value for the feedback metric. JSON scalars ("1", "true") are sent with their JSON type; anything else is sent as a string.', + }, comment: { type: 'string', description: 'Comment explaining the feedback' }, correction: { type: 'json', description: 'Corrected output for the run' }, feedbackSourceType: { @@ -491,8 +522,9 @@ Common patch fields: outputs, end_time, status, error`, key: { type: 'string', description: 'Feedback metric name (create feedback)' }, score: { type: 'number', description: 'Score recorded for the feedback (create feedback)' }, value: { - type: 'string', - description: 'Categorical value recorded for the feedback (create feedback)', + type: 'json', + description: + 'Categorical value recorded for the feedback — string, number, boolean, or JSON object (create feedback)', }, comment: { type: 'string', description: 'Comment recorded for the feedback (create feedback)' }, createdAt: { type: 'string', description: 'When the feedback was created (create feedback)' }, diff --git a/apps/sim/blocks/blocks/linkedin.ts b/apps/sim/blocks/blocks/linkedin.ts index 97a47dc9fa3..c7fb57790c5 100644 --- a/apps/sim/blocks/blocks/linkedin.ts +++ b/apps/sim/blocks/blocks/linkedin.ts @@ -83,6 +83,7 @@ export const LinkedInBlock: BlockConfig = { options: [ { label: 'Public', id: 'PUBLIC' }, { label: 'Connections Only', id: 'CONNECTIONS' }, + { label: 'Logged-In Members Only', id: 'LOGGED_IN' }, ], condition: { field: 'operation', @@ -126,11 +127,19 @@ export const LinkedInBlock: BlockConfig = { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'LinkedIn access token' }, text: { type: 'string', description: 'Post text content' }, - visibility: { type: 'string', description: 'Post visibility (PUBLIC or CONNECTIONS)' }, + visibility: { + type: 'string', + description: 'Post visibility (PUBLIC, CONNECTIONS, or LOGGED_IN)', + }, }, outputs: { success: { type: 'boolean', description: 'Operation success status' }, postId: { type: 'string', description: 'Created post ID' }, + postUrl: { + type: 'string', + description: + 'LinkedIn URL of the created post. Viewable by an authorized LinkedIn member — not a guaranteed public permalink', + }, profile: { type: 'json', description: 'LinkedIn profile information' }, error: { type: 'string', description: 'Error message if operation failed' }, }, @@ -210,7 +219,7 @@ export const LinkedInBlockMeta = { description: 'Draft and share a post to your LinkedIn feed with a hook, value, and a clear call-to-action.', content: - '# Publish LinkedIn Post\n\nWrite and publish an engagement-optimized post to your LinkedIn feed.\n\n## Steps\n1. Take the source idea, article, or update to post about.\n2. Draft post text with a strong first-line hook, two to three lines of value or insight, and a clear call-to-action, kept within LinkedIn length limits.\n3. Share Post with the drafted text and a visibility of PUBLIC or CONNECTIONS.\n\n## Output\nConfirmation the post was shared, the created post ID, and the final post text used.', + '# Publish LinkedIn Post\n\nWrite and publish an engagement-optimized post to your LinkedIn feed.\n\n## Steps\n1. Take the source idea, article, or update to post about.\n2. Draft post text with a strong first-line hook, two to three lines of value or insight, and a clear call-to-action, kept within LinkedIn length limits.\n3. Share Post with the drafted text and a visibility of PUBLIC or CONNECTIONS.\n\n## Output\nConfirmation the post was shared, the created post ID and URL, and the final post text used.', }, { name: 'repurpose-content-to-post', diff --git a/apps/sim/blocks/blocks/new_relic.ts b/apps/sim/blocks/blocks/new_relic.ts index 5a4cc80ba91..53686d5e9be 100644 --- a/apps/sim/blocks/blocks/new_relic.ts +++ b/apps/sim/blocks/blocks/new_relic.ts @@ -285,9 +285,15 @@ Return ONLY the numeric timestamp - no explanations, no extra text.`, config: { tool: (params) => String(params.operation || 'new_relic_nrql_query'), params: (params) => { + /** + * `timeout` is reserved by the tool request transport as the outbound fetch + * deadline in milliseconds, so the seconds-valued subBlock is remapped onto a + * tool-specific parameter and cleared here to keep it off the transport. + */ const baseParams = { apiKey: params.apiKey, region: params.region || 'us', + timeout: undefined, } switch (params.operation) { @@ -296,7 +302,7 @@ Return ONLY the numeric timestamp - no explanations, no extra text.`, ...baseParams, accountId: Number(params.accountId), nrql: params.nrql, - timeout: params.timeout ? Number(params.timeout) : undefined, + queryTimeout: params.timeout ? Number(params.timeout) : undefined, } case 'new_relic_search_entities': diff --git a/apps/sim/blocks/blocks/qdrant.test.ts b/apps/sim/blocks/blocks/qdrant.test.ts new file mode 100644 index 00000000000..675d1d9b875 --- /dev/null +++ b/apps/sim/blocks/blocks/qdrant.test.ts @@ -0,0 +1,30 @@ +/** + * @vitest-environment node + * + * The block's skill `content` is prompt text an LLM reads and acts on, so it is + * held to the same contract as `outputs`: it may only promise fields the tool + * actually returns. Qdrant's upsert endpoint returns an `UpdateResult` + * (`operation_id`, `status`) and no count of the points written. + */ +import { describe, expect, it } from 'vitest' +import { QdrantBlockMeta } from '@/blocks/blocks/qdrant' +import { UPSERT_RESULT_OUTPUT_PROPERTIES } from '@/tools/qdrant/types' + +const upsertSkill = QdrantBlockMeta.skills.find((skill) => skill.name === 'upsert-points') + +describe('QdrantBlockMeta upsert-points skill', () => { + it('is registered', () => { + expect(upsertSkill).toBeDefined() + }) + + it('does not instruct the model to read an upserted count the API never returns', () => { + expect(Object.keys(UPSERT_RESULT_OUTPUT_PROPERTIES)).toEqual(['operation_id', 'status']) + expect(upsertSkill?.content).not.toMatch(/upserted count/i) + expect(upsertSkill?.content).not.toMatch(/how many points were upserted/i) + }) + + it('points the model at the fields the upsert response does carry', () => { + expect(upsertSkill?.content).toMatch(/operation_id/) + expect(upsertSkill?.content).toMatch(/status/) + }) +}) diff --git a/apps/sim/blocks/blocks/qdrant.ts b/apps/sim/blocks/blocks/qdrant.ts index 45a8a605e0d..6f3c0efb970 100644 --- a/apps/sim/blocks/blocks/qdrant.ts +++ b/apps/sim/blocks/blocks/qdrant.ts @@ -44,13 +44,11 @@ export const QdrantBlock: BlockConfig = { ], value: () => 'upsert', }, - // Upsert fields { id: 'url', title: 'Qdrant URL', type: 'short-input', placeholder: 'http://localhost:6333', - condition: { field: 'operation', value: 'upsert' }, required: true, }, { @@ -58,9 +56,9 @@ export const QdrantBlock: BlockConfig = { title: 'Collection', type: 'short-input', placeholder: 'my-collection', - condition: { field: 'operation', value: 'upsert' }, required: true, }, + // Upsert fields { id: 'points', title: 'Points', @@ -95,22 +93,6 @@ Return ONLY the JSON array.`, }, }, // Search fields - { - id: 'url', - title: 'Qdrant URL', - type: 'short-input', - placeholder: 'http://localhost:6333', - condition: { field: 'operation', value: 'search' }, - required: true, - }, - { - id: 'collection', - title: 'Collection', - type: 'short-input', - placeholder: 'my-collection', - condition: { field: 'operation', value: 'search' }, - required: true, - }, { id: 'vector', title: 'Query Vector', @@ -175,22 +157,6 @@ Return ONLY the JSON object.`, condition: { field: 'operation', value: 'search' }, }, // Fetch fields - { - id: 'url', - title: 'Qdrant URL', - type: 'short-input', - placeholder: 'http://localhost:6333', - condition: { field: 'operation', value: 'fetch' }, - required: true, - }, - { - id: 'collection', - title: 'Collection', - type: 'short-input', - placeholder: 'my-collection', - condition: { field: 'operation', value: 'fetch' }, - required: true, - }, { id: 'ids', title: 'IDs', @@ -218,7 +184,6 @@ Return ONLY the JSON object.`, type: 'short-input', placeholder: 'Your Qdrant API key (optional)', password: true, - required: true, }, ], @@ -257,8 +222,6 @@ Return ONLY the JSON object.`, }, outputs: { - matches: { type: 'json', description: 'Search matches' }, - upsertedCount: { type: 'number', description: 'Upserted count' }, data: { type: 'json', description: 'Response data' }, status: { type: 'string', description: 'Operation status' }, }, @@ -340,7 +303,7 @@ export const QdrantBlockMeta = { name: 'upsert-points', description: 'Insert or update vector points with payload metadata into a Qdrant collection.', content: - '# Upsert Points\n\nLoad vectors into a Qdrant collection.\n\n## Steps\n1. Use the Upsert operation with the Qdrant URL, Collection name, and API Key.\n2. Provide Points as a JSON array, each with an id, a vector matching the collection dimension, and an optional payload of metadata for later filtering.\n3. Confirm the upserted count from the response.\n\n## Output\nReport how many points were upserted into which collection and surface any payload validation issues.', + '# Upsert Points\n\nLoad vectors into a Qdrant collection.\n\n## Steps\n1. Use the Upsert operation with the Qdrant URL, Collection name, and API Key.\n2. Provide Points as a JSON array, each with an id, a vector matching the collection dimension, and an optional payload of metadata for later filtering.\n3. Confirm the response status (`acknowledged` or `completed`) and keep the `operation_id` if you need to track an async write.\n\n## Output\nReport the upsert status and operation_id, which collection was targeted, and surface any payload validation issues. Qdrant does not return a count of points written \u2014 if you cite a number, cite the number of points you sent.', }, { name: 'search-vectors', diff --git a/apps/sim/blocks/blocks/serper.ts b/apps/sim/blocks/blocks/serper.ts index 758e23a81c5..69ea7ae5854 100644 --- a/apps/sim/blocks/blocks/serper.ts +++ b/apps/sim/blocks/blocks/serper.ts @@ -37,12 +37,14 @@ export const SerperBlock: BlockConfig = { title: 'Search Type', type: 'dropdown', options: [ - { label: 'search', id: 'search' }, - { label: 'news', id: 'news' }, - { label: 'places', id: 'places' }, - { label: 'images', id: 'images' }, - { label: 'videos', id: 'videos' }, - { label: 'shopping', id: 'shopping' }, + { label: 'Search', id: 'search' }, + { label: 'News', id: 'news' }, + { label: 'Places', id: 'places' }, + { label: 'Images', id: 'images' }, + { label: 'Videos', id: 'videos' }, + { label: 'Shopping', id: 'shopping' }, + { label: 'Scholar', id: 'scholar' }, + { label: 'Patents', id: 'patents' }, ], value: () => 'search', }, @@ -106,7 +108,23 @@ export const SerperBlock: BlockConfig = { type: { type: 'string', description: 'Search type' }, }, outputs: { - searchResults: { type: 'json', description: 'Search results data' }, + searchResults: { + type: 'json', + description: + 'Results for the requested vertical: [{title, link, snippet, position, date, imageUrl, thumbnailUrl, source, channel, rating, ratingCount, address, latitude, longitude, category, phoneNumber, website, price, delivery, duration}]. Only title and position are always present; the rest depend on the vertical', + }, + knowledgeGraph: { + type: 'json', + description: 'Google knowledge panel. Web search vertical only, when Google renders one', + }, + peopleAlsoAsk: { + type: 'json', + description: 'Google "People also ask" entries. Web search vertical only', + }, + relatedSearches: { + type: 'json', + description: 'Google "Related searches" queries. Web search vertical only', + }, }, } @@ -187,7 +205,7 @@ export const SerperBlockMeta = { description: 'Run a Serper web search on a topic and synthesize the top organic results into a sourced summary.', content: - '# Research Web Topic\n\nGather current web results on a topic and turn them into a concise, sourced briefing.\n\n## Steps\n1. Run the search operation with a focused query. Set the country and language for the target audience and choose a result count (10 to 100) appropriate to the depth needed.\n2. Read the organic results, titles, snippets, and any answer box or knowledge graph data.\n3. Synthesize the findings, noting agreement and disagreement across sources.\n\n## Output\nReturn a short summary of what the web says about the topic, each key claim linked to the result URL it came from.', + '# Research Web Topic\n\nGather current web results on a topic and turn them into a concise, sourced briefing.\n\n## Steps\n1. Run the search operation with a focused query. Set the country and language for the target audience and choose a result count (10 to 100) appropriate to the depth needed.\n2. Read the returned results: titles, links, and snippets.\n3. Synthesize the findings, noting agreement and disagreement across sources.\n\n## Output\nReturn a short summary of what the web says about the topic, each key claim linked to the result URL it came from.', }, { name: 'monitor-news-mentions', @@ -208,7 +226,7 @@ export const SerperBlockMeta = { description: 'Use the Serper places operation to find local businesses for a query and area, and rank them.', content: - '# Find Local Businesses\n\nPull Google Maps style local results for a query in a target area.\n\n## Steps\n1. Run the places operation with a query that includes the business type and location (for example coffee shops in Seattle), setting the country and language to scope results.\n2. Read each place result: name, address, rating, review count, category, and phone or website where present.\n3. Rank or filter the results by rating, review volume, or proximity to the target area.\n\n## Output\nReturn a ranked list of local businesses with name, address, rating, and review count, noting the top candidates for the query.', + '# Find Local Businesses\n\nPull Google Maps style local results for a query in a target area.\n\n## Steps\n1. Run the places operation with a query that includes the business type and location (for example coffee shops in Seattle), setting the country and language to scope results.\n2. Read each place result: title (the business name), address, rating, ratingCount (the review count), category, phoneNumber, website, and latitude/longitude where present. Places results carry no link and no snippet.\n3. Rank or filter the results by rating, review volume, or by latitude/longitude distance from the target area.\n\n## Output\nReturn a ranked list of local businesses with name, address, rating, and review count, noting the top candidates for the query.', }, ], } as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/sixtyfour.ts b/apps/sim/blocks/blocks/sixtyfour.ts index fd403512a47..abc5e1ee93b 100644 --- a/apps/sim/blocks/blocks/sixtyfour.ts +++ b/apps/sim/blocks/blocks/sixtyfour.ts @@ -1,5 +1,6 @@ import { SixtyfourIcon } from '@/components/icons' import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import { SIXTYFOUR_ENRICH_TIMEOUT_MS } from '@/tools/sixtyfour/types' const PHONE_LOOKUP_FIELD = ['name', 'linkedinUrl', 'emailInput'] as const const EMAIL_LOOKUP_FIELD = ['name', 'linkedinUrl', 'phoneInput'] as const @@ -144,9 +145,23 @@ export const SixtyfourBlock: BlockConfig = { type: 'long-input', placeholder: '{"email": "Email address", "phone": "Phone number", "company": "Company name", "title": "Job title"}', - required: { field: 'operation', value: 'enrich_lead' }, condition: { field: 'operation', value: 'enrich_lead' }, }, + { + id: 'leadTier', + title: 'Research Tier', + type: 'dropdown', + options: [ + { label: 'Micro', id: 'micro' }, + { label: 'Low', id: 'low' }, + { label: 'Medium', id: 'medium' }, + { label: 'High', id: 'high' }, + { label: 'Extra High', id: 'xhigh' }, + ], + placeholder: 'Low (default)', + condition: { field: 'operation', value: 'enrich_lead' }, + mode: 'advanced', + }, { id: 'leadResearchPlan', title: 'Research Plan', @@ -201,6 +216,20 @@ export const SixtyfourBlock: BlockConfig = { condition: { field: 'operation', value: 'enrich_company' }, mode: 'advanced', }, + { + id: 'companyTier', + title: 'Research Tier', + type: 'dropdown', + options: [ + { label: 'Micro', id: 'micro' }, + { label: 'Low', id: 'low' }, + { label: 'Medium', id: 'medium' }, + { label: 'High', id: 'high' }, + ], + placeholder: 'Low (default)', + condition: { field: 'operation', value: 'enrich_company' }, + mode: 'advanced', + }, { id: 'companyResearchPlan', title: 'Research Plan', @@ -228,12 +257,16 @@ export const SixtyfourBlock: BlockConfig = { } else if (params.operation === 'find_email') { if (params.phoneInput) result.phone = params.phoneInput } else if (params.operation === 'enrich_lead') { - result.leadInfo = params.leadInfo - result.struct = params.leadStruct + if (params.leadInfo) result.leadInfo = params.leadInfo + if (params.leadStruct) result.struct = params.leadStruct + if (params.leadTier) result.tier = params.leadTier if (params.leadResearchPlan) result.researchPlan = params.leadResearchPlan + result.timeout = SIXTYFOUR_ENRICH_TIMEOUT_MS } else if (params.operation === 'enrich_company') { - result.targetCompany = params.targetCompany - result.struct = params.companyStruct + if (params.targetCompany) result.targetCompany = params.targetCompany + if (params.companyStruct) result.struct = params.companyStruct + if (params.companyTier) result.tier = params.companyTier + result.timeout = SIXTYFOUR_ENRICH_TIMEOUT_MS if (params.findPeople !== undefined) result.findPeople = Boolean(params.findPeople) if (params.fullOrgChart !== undefined) result.fullOrgChart = Boolean(params.fullOrgChart) if (params.peopleFocusPrompt) result.peopleFocusPrompt = params.peopleFocusPrompt @@ -259,6 +292,7 @@ export const SixtyfourBlock: BlockConfig = { mode: { type: 'string', description: 'Email mode (PROFESSIONAL or PERSONAL)' }, leadInfo: { type: 'string', description: 'Lead information JSON' }, leadStruct: { type: 'string', description: 'Fields to collect for lead' }, + leadTier: { type: 'string', description: 'Research tier for lead enrichment' }, leadResearchPlan: { type: 'string', description: 'Research plan for lead enrichment' }, targetCompany: { type: 'string', description: 'Company information JSON' }, companyStruct: { type: 'string', description: 'Fields to collect for company' }, @@ -266,6 +300,7 @@ export const SixtyfourBlock: BlockConfig = { fullOrgChart: { type: 'boolean', description: 'Retrieve full org chart' }, peopleFocusPrompt: { type: 'string', description: 'People focus description' }, companyLeadStruct: { type: 'string', description: 'Lead schema for company enrichment' }, + companyTier: { type: 'string', description: 'Research tier for company enrichment' }, companyResearchPlan: { type: 'string', description: 'Research plan for company enrichment' }, }, diff --git a/apps/sim/blocks/blocks/trigger_dev.ts b/apps/sim/blocks/blocks/trigger_dev.ts index 159cb96da05..fa1b1e60682 100644 --- a/apps/sim/blocks/blocks/trigger_dev.ts +++ b/apps/sim/blocks/blocks/trigger_dev.ts @@ -1006,6 +1006,13 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, scoped(params.version, ['trigger_dev_list_runs']) ?? scoped(params.deploymentVersion, ['trigger_dev_promote_deployment']) result.data = scoped(params.waitpointData, ['trigger_dev_complete_waitpoint_token']) + /** + * `timeout` is reserved by the tool request transport as the outbound fetch + * deadline in milliseconds, so the waitpoint duration subBlock is remapped onto a + * tool-specific parameter and cleared here to keep it off the transport. + */ + result.timeout = undefined + result.tokenTimeout = scoped(params.timeout, ['trigger_dev_create_waitpoint_token']) result.period = scoped(params.period, CREATED_AT_FILTER_OPERATIONS) result.from = scoped(params.from, CREATED_AT_FILTER_OPERATIONS) result.to = scoped(params.to, CREATED_AT_FILTER_OPERATIONS) @@ -1219,6 +1226,11 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, description: 'Environment variable or queue name (env var and queue operations)', }, value: { type: 'string', description: 'Value of the environment variable (Get Env Var)' }, + isSecret: { + type: 'boolean', + description: + 'Whether the environment variable is a secret, meaning its value is redacted (Get Env Var)', + }, success: { type: 'boolean', description: 'Whether the operation succeeded (env var operations, Complete Waitpoint Token)', diff --git a/apps/sim/blocks/blocks/twilio_voice.ts b/apps/sim/blocks/blocks/twilio_voice.ts index 2d203b97fe1..50a68f33fab 100644 --- a/apps/sim/blocks/blocks/twilio_voice.ts +++ b/apps/sim/blocks/blocks/twilio_voice.ts @@ -348,8 +348,15 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n const baseParams = { ...rest } + /** + * `timeout` is reserved by the tool request transport as the outbound fetch + * deadline in milliseconds, so the seconds-valued subBlock is remapped onto a + * tool-specific parameter and cleared here to keep it off the transport. + */ + baseParams.timeout = undefined + if (operation === 'make_call' && timeout) { - baseParams.timeout = Number.parseInt(timeout, 10) + baseParams.callTimeout = Number.parseInt(timeout, 10) } if (operation === 'make_call' && record !== undefined && record !== null) { diff --git a/apps/sim/blocks/blocks/vercel.ts b/apps/sim/blocks/blocks/vercel.ts index 6f4d6a43888..6e3f284147b 100644 --- a/apps/sim/blocks/blocks/vercel.ts +++ b/apps/sim/blocks/blocks/vercel.ts @@ -339,6 +339,8 @@ export const VercelBlock: BlockConfig = { { label: 'Error', id: 'ERROR' }, { label: 'Queued', id: 'QUEUED' }, { label: 'Canceled', id: 'CANCELED' }, + { label: 'Initializing', id: 'INITIALIZING' }, + { label: 'Blocked', id: 'BLOCKED' }, ], condition: { field: 'operation', value: 'list_deployments' }, mode: 'advanced', @@ -607,7 +609,6 @@ export const VercelBlock: BlockConfig = { { label: 'SvelteKit', id: 'sveltekit' }, { label: 'Astro', id: 'astro' }, { label: 'Gatsby', id: 'gatsby' }, - { label: 'Other', id: 'other' }, ], condition: { field: 'operation', value: ['create_project', 'update_project'] }, mode: 'advanced', @@ -2197,7 +2198,16 @@ export const VercelBlock: BlockConfig = { description: 'Deployment state', condition: { field: 'operation', - value: ['get_deployment', 'create_deployment', 'cancel_deployment', 'delete_deployment'], + value: ['cancel_deployment', 'delete_deployment'], + }, + }, + readyState: { + type: 'string', + description: + 'Deployment ready state: BLOCKED, BUILDING, CANCELED, ERROR, INITIALIZING, QUEUED, or READY', + condition: { + field: 'operation', + value: ['get_deployment', 'create_deployment'], }, }, deleted: { @@ -2206,12 +2216,10 @@ export const VercelBlock: BlockConfig = { condition: { field: 'operation', value: [ - 'delete_deployment', 'delete_project', 'remove_project_domain', 'delete_domain', 'delete_dns_record', - 'delete_alias', 'delete_env_var', 'delete_webhook', 'delete_edge_config', @@ -2231,6 +2239,23 @@ export const VercelBlock: BlockConfig = { description: 'Whether the deployment was promoted to production', condition: { field: 'operation', value: 'promote_deployment' }, }, + status: { + type: 'string', + description: + 'Status reported by Vercel: deployment status, check status (registered, running, completed), deletion status, or edge config update status', + condition: { + field: 'operation', + value: [ + 'update_edge_config_items', + 'get_deployment', + 'cancel_deployment', + 'delete_alias', + 'create_check', + 'get_check', + 'update_check', + ], + }, + }, count: { type: 'number', description: 'Number of items returned', diff --git a/apps/sim/executor/handlers/pi/search/normalize.test.ts b/apps/sim/executor/handlers/pi/search/normalize.test.ts index 3d04fb63728..43be0964f33 100644 --- a/apps/sim/executor/handlers/pi/search/normalize.test.ts +++ b/apps/sim/executor/handlers/pi/search/normalize.test.ts @@ -13,6 +13,7 @@ import { PI_SEARCH_MAX_RESULTS, PI_SEARCH_MAX_SNIPPET_LENGTH, PI_SEARCH_MAX_TITLE_LENGTH, + PI_SEARCH_TIMEOUT_MS, PI_SEARCH_TOOL_PARAMETERS, PI_SEARCH_TRUNCATED_MESSAGE, parsePiSearchArgs, @@ -90,6 +91,10 @@ describe('buildPiSearchProviderArgs', () => { expect(buildPiSearchProviderArgs('firecrawl', query)).toEqual({ query: 'ts 5.9 release notes', limit: 3, + // Firecrawl's own server-side budget, carried under a name that does not collide with the + // transport's reserved `timeout`. The sandbox path sends the same value, and + // parity.test.ts is what holds the two paths together. + firecrawlTimeout: PI_SEARCH_TIMEOUT_MS, }) }) }) diff --git a/apps/sim/executor/handlers/pi/search/normalize.ts b/apps/sim/executor/handlers/pi/search/normalize.ts index af63a96a7c0..c4e937a120d 100644 --- a/apps/sim/executor/handlers/pi/search/normalize.ts +++ b/apps/sim/executor/handlers/pi/search/normalize.ts @@ -156,7 +156,8 @@ export function parsePiSearchArgs(args: Record): PiSearchQuery * default — so this is the one place the mapping is written down. * * Note two declared-type traps that make this look impossible from the tool definitions: - * `firecrawl_search` declares only `query`/`apiKey` yet its request body reads `params.limit`, and + * `firecrawl_search` declares only `query`/`apiKey` yet its request body reads `params.limit` and + * `params.firecrawlTimeout`, and * `exa_search` declares `text` as a boolean yet the object form is what requests page text. Both * work because `executeTool` hands the raw parameter bag to `body()`. */ @@ -172,7 +173,12 @@ export function buildPiSearchProviderArgs( case 'parallel': return { objective: query, max_results: numResults } case 'firecrawl': - return { query, limit: numResults } + /** + * `firecrawlTimeout` becomes Firecrawl's own server-side `timeout` body field. It is named + * apart from `timeout` because the transport reads `params.timeout` as the outbound fetch + * deadline, so a single key cannot mean both. The sandbox path hardcodes the same value. + */ + return { query, limit: numResults, firecrawlTimeout: PI_SEARCH_TIMEOUT_MS } } } diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 786ce8e46cb..9e6f6a3a494 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -385,7 +385,7 @@ export const SCOPE_DESCRIPTIONS: Record = { 'webhooks:full': 'Full access to manage Pipedrive webhooks', // LinkedIn scopes - w_member_social: 'Access LinkedIn profile', + w_member_social: 'Post, comment, and like posts on your behalf', // Instagram scopes (Business Login for Instagram) instagram_business_basic: 'Access Instagram professional profile and media', diff --git a/apps/sim/lib/webhooks/polling/hubspot-path-safety.test.ts b/apps/sim/lib/webhooks/polling/hubspot-path-safety.test.ts new file mode 100644 index 00000000000..d3458c92d92 --- /dev/null +++ b/apps/sim/lib/webhooks/polling/hubspot-path-safety.test.ts @@ -0,0 +1,291 @@ +/** + * @vitest-environment node + * + * The HubSpot poller builds two provider URLs from trigger config, so the + * reflective `request.url` probe in `tools/hubspot/path_safety.test.ts` cannot + * see either one: + * + * - `/crm/v3/objects/{objectType}/search`, where `objectType` resolves from the + * free-text `customObjectTypeId` field declared in `triggers/hubspot/poller.ts` + * - `/crm/v3/lists/{listId}/memberships/join-order` + * + * Both run in a background poller holding the workspace HubSpot token. + * `encodeURIComponent` does not neutralize a dot segment — `.` and `..` are + * unreserved and the WHATWG parser removes them after decoding — so a value of + * `..` pops `objects`/`lists` and re-aims the authenticated request at a + * sibling endpoint. + * + * Assertions resolve the outgoing URL through `new URL(...)`, the same + * normalization `fetch` applies, and compare whole-pathname segment shape. + */ +import type { Logger } from '@sim/logger' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockResolveOAuthCredential, + mockMarkWebhookFailed, + mockMarkWebhookSuccess, + mockUpdateWebhookProviderConfig, + mockProcessPolledWebhookEvent, + mockIdempotencyExecute, +} = vi.hoisted(() => ({ + mockResolveOAuthCredential: vi.fn(), + mockMarkWebhookFailed: vi.fn(), + mockMarkWebhookSuccess: vi.fn(), + mockUpdateWebhookProviderConfig: vi.fn(), + mockProcessPolledWebhookEvent: vi.fn(), + mockIdempotencyExecute: vi.fn(), +})) + +vi.mock('@/lib/webhooks/polling/utils', () => ({ + resolveOAuthCredential: mockResolveOAuthCredential, + markWebhookFailed: mockMarkWebhookFailed, + markWebhookSuccess: mockMarkWebhookSuccess, + updateWebhookProviderConfig: mockUpdateWebhookProviderConfig, +})) + +vi.mock('@/lib/webhooks/processor', () => ({ + processPolledWebhookEvent: mockProcessPolledWebhookEvent, +})) + +vi.mock('@/lib/core/idempotency/service', () => ({ + pollingIdempotency: { execute: mockIdempotencyExecute }, +})) + +import { hubspotPollingHandler } from '@/lib/webhooks/polling/hubspot' + +const ORIGIN = 'https://api.hubapi.com' + +/** + * Values that must never reach the provider URL. The percent-encoded dot forms + * are included deliberately: the tools suite treats them as merely *inert* + * (encoding them again is enough), but neither is a legal HubSpot object type + * or list id, so the poller rejects rather than forwards them. + */ +const REJECTED = ['..', '.', '%2e%2e', '..%2f..', 'a/b', ' .. ', '../..'] as const + +/** + * The three legal `objectType` spellings from HubSpot's Schemas API, plus a + * bare custom object name. None contains a separator or is a dot segment. + */ +const LEGITIMATE = ['p7878787_my_object', '2-123456', '0-1', 'my_object'] as const + +/** + * Keys inherited from `Object.prototype`. Both `BUILT_IN_PATH` and + * `DEFAULT_PROPERTIES` are plain object literals, and `in` walks the prototype + * chain exactly as bracket access does — so these names resolved to a function + * or an object instead of missing, and the built-in branch ran for an object + * type that is not built in. Each is a legal HubSpot custom-object name, so the + * correct outcome is the ordinary custom-object path. + */ +const PROTOTYPE_KEYS = [ + 'constructor', + '__proto__', + 'toString', + 'hasOwnProperty', + 'valueOf', +] as const + +function requestBody(): { properties: string[] } { + return JSON.parse((fetchMock.mock.calls[0][1] as { body: string }).body) +} + +const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), +} as unknown as Logger + +let fetchMock: ReturnType + +beforeEach(() => { + vi.clearAllMocks() + mockResolveOAuthCredential.mockResolvedValue('inert-token') + mockUpdateWebhookProviderConfig.mockResolvedValue(undefined) + mockMarkWebhookSuccess.mockResolvedValue(undefined) + mockMarkWebhookFailed.mockResolvedValue(undefined) + fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ results: [], paging: {} }), + text: async () => '', + }) + vi.stubGlobal('fetch', fetchMock) +}) + +function poll(providerConfig: Record) { + return hubspotPollingHandler.pollWebhook({ + webhookData: { id: 'wh-1', providerConfig } as any, + workflowData: { id: 'wf-1' } as any, + requestId: 'req-1', + logger, + }) +} + +function outgoingUrl(): URL { + expect(fetchMock).toHaveBeenCalledTimes(1) + return new URL(fetchMock.mock.calls[0][0] as string) +} + +function searchConfig(customObjectTypeId: string) { + return { + objectType: 'custom', + customObjectTypeId, + eventType: 'created', + lastSeenTimestampMs: '1000', + } +} + +describe('HubSpot poller search-path safety', () => { + it('maps a built-in slug to its documented plural constant', async () => { + const result = await poll({ + objectType: 'contact', + eventType: 'created', + lastSeenTimestampMs: '1000', + }) + + expect(result).toBe('success') + const url = outgoingUrl() + expect(url.origin).toBe(ORIGIN) + expect(url.pathname.split('/')).toEqual(['', 'crm', 'v3', 'objects', 'contacts', 'search']) + }) + + it.each(LEGITIMATE)('passes objectType=%j through byte-identically', async (objectType) => { + const result = await poll(searchConfig(objectType)) + + expect(result).toBe('success') + expect(fetchMock.mock.calls[0][0]).toBe(`${ORIGIN}/crm/v3/objects/${objectType}/search`) + expect(outgoingUrl().pathname.split('/')).toEqual([ + '', + 'crm', + 'v3', + 'objects', + objectType, + 'search', + ]) + }) + + it.each(REJECTED)('never calls HubSpot for objectType=%j', async (objectType) => { + const result = await poll(searchConfig(objectType)) + + expect(fetchMock).not.toHaveBeenCalled() + expect(result).toBe('failure') + expect(mockMarkWebhookFailed).toHaveBeenCalledWith('wh-1', logger) + }) + + /** + * `resolveObjectType` falls back to `''` when `customObjectTypeId` is blank. + * `pollSearchBased` already names that as a config error before any URL is + * built; this pins the behavior so a future refactor cannot let an empty path + * segment through. + */ + it.each(['', ' ', undefined])( + 'fails with a named config error for customObjectTypeId=%j', + async (customObjectTypeId) => { + const result = await poll({ + objectType: 'custom', + customObjectTypeId, + eventType: 'created', + lastSeenTimestampMs: '1000', + }) + + expect(fetchMock).not.toHaveBeenCalled() + expect(result).toBe('failure') + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('req-1'), + expect.objectContaining({ message: expect.stringContaining('missing objectType') }) + ) + } + ) +}) + +describe('HubSpot poller inherited-key lookups', () => { + it.each(PROTOTYPE_KEYS)( + 'treats objectType=%j as an ordinary custom object type, not a built-in', + async (objectType) => { + const result = await poll(searchConfig(objectType)) + + expect(logger.error).not.toHaveBeenCalled() + expect(result).toBe('success') + expect(fetchMock.mock.calls[0][0]).toBe(`${ORIGIN}/crm/v3/objects/${objectType}/search`) + expect(outgoingUrl().pathname.split('/')).toEqual([ + '', + 'crm', + 'v3', + 'objects', + objectType, + 'search', + ]) + } + ) + + it.each(PROTOTYPE_KEYS)( + 'requests only the baseline properties for objectType=%j, never a built-in default set', + async (objectType) => { + await poll(searchConfig(objectType)) + + expect(requestBody().properties).toEqual(['createdate']) + } + ) + + it('still applies the built-in default property set for a real built-in slug', async () => { + const result = await poll({ + objectType: 'deal', + eventType: 'created', + lastSeenTimestampMs: '1000', + }) + + expect(result).toBe('success') + const { properties } = requestBody() + expect(properties).toContain('dealname') + expect(properties).toContain('createdate') + expect(properties.length).toBeGreaterThan(1) + }) +}) + +describe('HubSpot poller list-membership path safety', () => { + it.each(['12345', '1e2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b'])( + 'passes listId=%j through byte-identically', + async (listId) => { + const result = await poll({ objectType: 'list_membership', listId }) + + expect(result).toBe('success') + expect(fetchMock.mock.calls[0][0]).toBe( + `${ORIGIN}/crm/v3/lists/${listId}/memberships/join-order?limit=100` + ) + expect(outgoingUrl().pathname.split('/')).toEqual([ + '', + 'crm', + 'v3', + 'lists', + listId, + 'memberships', + 'join-order', + ]) + } + ) + + it.each(REJECTED)('never calls HubSpot for listId=%j', async (listId) => { + const result = await poll({ objectType: 'list_membership', listId }) + + expect(fetchMock).not.toHaveBeenCalled() + expect(result).toBe('failure') + expect(mockMarkWebhookFailed).toHaveBeenCalledWith('wh-1', logger) + }) + + /** Blank `listId` is already a named config error; pinned for the same reason. */ + it.each(['', ' ', undefined])( + 'fails with a named config error for listId=%j', + async (listId) => { + const result = await poll({ objectType: 'list_membership', listId }) + + expect(fetchMock).not.toHaveBeenCalled() + expect(result).toBe('failure') + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('req-1'), + expect.objectContaining({ message: expect.stringContaining('missing listId') }) + ) + } + ) +}) diff --git a/apps/sim/lib/webhooks/polling/hubspot.ts b/apps/sim/lib/webhooks/polling/hubspot.ts index a854f28511b..9a93ddb1749 100644 --- a/apps/sim/lib/webhooks/polling/hubspot.ts +++ b/apps/sim/lib/webhooks/polling/hubspot.ts @@ -1,6 +1,7 @@ import type { Logger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { pollingIdempotency } from '@/lib/core/idempotency/service' +import { validatePathSegment } from '@/lib/core/security/input-validation' import { getProviderConfig, type PollingProviderHandler, @@ -132,8 +133,39 @@ const VALID_OPERATORS = new Set([ 'NOT_HAS_PROPERTY', ]) +/** + * Guards a trigger-config value before it becomes a URL path segment. + * + * `objectType` and `listId` both originate in free-text trigger fields, and the + * poller interpolates them into a provider URL while holding the workspace + * HubSpot token. `encodeURIComponent` does not neutralize a dot segment — `.` + * and `..` are unreserved and the WHATWG parser removes them after decoding — + * so `'..'` pops `objects`/`lists` and re-aims the authenticated request. + * + * HubSpot's own charsets are narrower than this allowlist: object types are + * `p7878787_my_object`, `{meta-type}-{unique id}` (`2-123456`, `0-1`), or a bare + * `my_object`, and list ids are numeric or a GUID. Nothing legal is rejected. + * + * Throws, matching how every other config defect in this poller surfaces — + * `pollWebhook` catches, logs, and marks the webhook failed. + */ +function assertSafePathSegment(value: string, paramName: string): string { + const validation = validatePathSegment(value, { paramName }) + if (!validation.isValid) { + throw new Error(`HubSpot ${validation.error}`) + } + return value +} + +/** + * `Object.hasOwn` rather than `in`: `in` walks the prototype chain, so + * `objectType = 'constructor'` (or `'__proto__'`, `'toString'`, …) took the + * built-in branch and returned a function instead of a path. Those names are + * legal HubSpot custom-object spellings and belong on the ordinary custom + * path. + */ function resolveSearchPath(objectType: string): string { - if (objectType in BUILT_IN_PATH) { + if (Object.hasOwn(BUILT_IN_PATH, objectType)) { return BUILT_IN_PATH[objectType as HubSpotBuiltInObjectType] } return objectType @@ -519,7 +551,8 @@ function resolveRequestedProperties( if (trimmed) requested.add(trimmed) } - if (requested.size === 0 && objectType in BUILT_IN_PATH) { + // `Object.hasOwn`, not `in` — see `resolveSearchPath`. + if (requested.size === 0 && Object.hasOwn(BUILT_IN_PATH, objectType)) { for (const name of DEFAULT_PROPERTIES[objectType as HubSpotBuiltInObjectType]) { requested.add(name) } @@ -647,7 +680,8 @@ async function fetchHubSpotChanges(args: FetchArgs): Promise { const { listId, accessToken, initialAfter, pageLimit, requestId, logger } = args + const safeListId = assertSafePathSegment(listId, 'listId') const records: ListMembershipRecord[] = [] let after: string | undefined = initialAfter @@ -927,7 +962,7 @@ async function fetchListMembershipPages( while (pages < MAX_PAGES_PER_POLL) { const params = new URLSearchParams({ limit: String(HUBSPOT_PAGE_LIMIT) }) if (after) params.set('after', after) - const url = `https://api.hubapi.com/crm/v3/lists/${encodeURIComponent(listId)}/memberships/join-order?${params.toString()}` + const url = `https://api.hubapi.com/crm/v3/lists/${encodeURIComponent(safeListId)}/memberships/join-order?${params.toString()}` const response = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } }) if (!response.ok) { diff --git a/apps/sim/lib/workspace-files/shell-layout.test.ts b/apps/sim/lib/workspace-files/shell-layout.test.ts index 493cabb462d..edb5e249d18 100644 --- a/apps/sim/lib/workspace-files/shell-layout.test.ts +++ b/apps/sim/lib/workspace-files/shell-layout.test.ts @@ -17,7 +17,6 @@ function runShell(source: string) { .replace(/^\n/, '') .replace(/<\/html>$/, '') const script = SIM_ARTIFACT_SHELL.replace(/^