diff --git a/.tool-versions b/.tool-versions index 9dde475f271..8b05a5963ec 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -pnpm 10.12.4 +pnpm 10.29.3 nodejs 22.17.0 tilt 0.35.0 diff --git a/integrations/chat/Dockerfile b/integrations/chat/Dockerfile index 0395ec53c97..79876072c47 100644 --- a/integrations/chat/Dockerfile +++ b/integrations/chat/Dockerfile @@ -7,7 +7,7 @@ # docker run -it -e SECRET_SIGNAL_URL=https://chat.botpress.dev botpress-chat ARG NODE_VERSION=22.17.0 -ARG PNPM_VERSION=10.12.4 +ARG PNPM_VERSION=10.29.3 FROM node:${NODE_VERSION}-bullseye-slim AS base diff --git a/integrations/zoho/hub.md b/integrations/zoho/hub.md index b328ac53eea..bb85b5022f3 100644 --- a/integrations/zoho/hub.md +++ b/integrations/zoho/hub.md @@ -16,46 +16,45 @@ This Botpress integration allows seamless interaction with **Zoho CRM**. It enab - **Emails:** Send emails and associate them with records. - **Organization & User Management:** Retrieve organization details and user information. -## Register Your Application +## Connect Zoho CRM -Before making any API calls using the Zoho Botpress Integration, you must register your application with **Zoho CRM**. +Use the **Connect with OAuth** setup flow in Botpress. The integration will ask for your Zoho data center, redirect you to Zoho for consent, request the required CRM scopes, and securely store the returned OAuth tokens. -### **[Loom video walk through setting up the OAuth configuration.](https://www.loom.com/share/41c2811c047a48cbb08a2d1b0dc98f69?sid=8cb4d496-2cca-415d-be1d-536a87c73a3a)** +The Zoho OAuth app must allow the Botpress callback URL shown by your integration setup. It should use the stable callback path `/oauth/wizard/oauth-callback`; the OAuth `state` value is sent separately and should not be included in the registered redirect URI. -### Steps to Register +The scopes are requested during the authorization flow. They are not configured directly on the Zoho application: -1. **Go to the [Zoho Developer Console](https://accounts.zoho.com/developerconsole).** -2. Click **Add Client**. -3. Choose the client type: **Self Client**. -4. Click the **Generate Code** tab and enter the following scopes: +```text +ZohoCRM.modules.ALL,ZohoCRM.org.ALL,ZohoCRM.users.ALL,ZohoCRM.settings.ALL,ZohoCRM.send_mail.all.CREATE,ZohoCRM.files.CREATE,ZohoCRM.files.READ +``` - ``` - ZohoCRM.modules.ALL,ZohoCRM.org.ALL,ZohoCRM.users.ALL,ZohoCRM.settings.ALL,ZohoCRM.send_mail.all.CREATE,ZohoCRM.files.CREATE,ZohoCRM.files.READ - ``` +### Zoho Accounts Domains -5. Set the **time duration** to **10 minutes**. -6. Provide a scope description (This is not vital to the registration). -7. Click **Create**, select your CRM, and click **Create** again. -8. Download your **credentials file**. +| Region | Accounts URL | +| ----------- | ------------------------------- | +| US | `https://accounts.zoho.com` | +| AU | `https://accounts.zoho.com.au` | +| EU | `https://accounts.zoho.eu` | +| IN | `https://accounts.zoho.in` | +| CN | `https://accounts.zoho.com.cn` | +| JP | `https://accounts.zoho.jp` | +| CA (Canada) | `https://accounts.zohocloud.ca` | -### Generate Refresh Token +## Manual Configuration -Now, execute the following **cURL** command to obtain a refresh token. Ensure you use the **correct region URL** for OAuth authentication. +Manual configuration is available as an advanced fallback. Use it only if you need to provide your own Zoho OAuth client credentials and refresh token. -#### **Zoho Accounts Domains:** +### Register Your Application -| Region | Accounts URL | -| ----------------- | ------------------------------- | -| US | `https://accounts.zoho.com` | -| AU | `https://accounts.zoho.com.au` | -| EU | `https://accounts.zoho.eu` | -| IN | `https://accounts.zoho.in` | -| CN | `https://accounts.zoho.com.cn` | -| JP | `https://accounts.zoho.jp` | -| SA (Saudi Arabia) | `https://accounts.zoho.sa` | -| CA (Canada) | `https://accounts.zohocloud.ca` | +1. Go to the [Zoho Developer Console](https://accounts.zoho.com/developerconsole). +2. Click **Add Client**. +3. Choose a server-based client type. +4. Add the Botpress OAuth callback URL as an authorized redirect URI. +5. Save the client ID and client secret. -### Execute cURL Request +### Generate Refresh Token + +Create an authorization URL with the scopes above, `access_type=offline`, and `prompt=consent`. After granting consent, exchange the returned authorization code for OAuth tokens. Replace the placeholders (`CLIENT_ID`, `CLIENT_SECRET`, and `AUTHORIZATION_CODE`) with your actual values before executing the request. @@ -70,9 +69,7 @@ curl --request POST \ --data 'code=AUTHORIZATION_CODE' ``` -### Expected Response - -If the request is successful, you should receive a response similar to the following: +If successful, Zoho returns a response similar to: ```json { @@ -84,71 +81,13 @@ If the request is successful, you should receive a response similar to the follo } ``` -# Define the Zoho OAuth token endpoint based on your region - -$uri = "https://YOUR_REGION_ACCOUNT_URL/oauth/v2/token" - -# Define the request body with required parameters - -$body = @{ -grant_type = "authorization_code" -client_id = "YOUR_CLIENT_ID" -client_secret = "YOUR_CLIENT_SECRET" -redirect_uri = "YOUR_REDIRECT_URI" -code = "AUTHORIZATION_CODE" -} - -### Generate Refresh Token using PowerShell (Windows Users) - -⚠️ Use this PowerShell command if you're on Windows. Do NOT use cURL—this is for PowerShell only! ⚠️ - -#### Define the Zoho OAuth token endpoint based on your region - -$uri = "https://YOUR_REGION_ACCOUNT_URL/oauth/v2/token" - -# Define the request body with required parameters - -$body = @{ -grant_type = "authorization_code" -client_id = "YOUR_CLIENT_ID" -client_secret = "YOUR_CLIENT_SECRET" -redirect_uri = "YOUR_REDIRECT_URI" -code = "AUTHORIZATION_CODE" -} - -##### Convert body to URL-encoded form data - -``` -$body = $body | ForEach-Object { "$( $_.Key )=$( $_.Value )" } -join "&" -``` - -#### Send the POST request using Invoke-RestMethod - -``` -$response = Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -``` - -#### Output the response - -``` -$response -``` - -## Configure Zoho Botpress Integration - -Once you have the necessary credentials, navigate to the **Zoho Botpress Integration** configuration page and enter the following details: +Enter these values in the **Manual configuration** setup: - **Client ID** - **Client Secret** - **Refresh Token** - **Region** -This completes the registration and integration process for **Zoho Botpress**. You are now ready to start making authorized API calls. - ---- - -For more details, refer to the [Zoho API Documentation](https://www.zoho.com/crm/developer/docs/). - --- ## API Functions & Usage diff --git a/integrations/zoho/integration.definition.ts b/integrations/zoho/integration.definition.ts index 547e7e72c0a..a09edec0f3f 100644 --- a/integrations/zoho/integration.definition.ts +++ b/integrations/zoho/integration.definition.ts @@ -1,5 +1,4 @@ import { IntegrationDefinition, z } from '@botpress/sdk' - import { makeApiCallInputSchema, makeApiCallOutputSchema, @@ -36,25 +35,43 @@ import { getFileInputSchema, getFileOutputSchema, } from './src/misc/custom-schemas' +import { DATA_CENTERS } from './src/misc/data-centers' +import { zohoCredentialsStateSchema, zohoOAuthWizardStateSchema } from './src/misc/oauth-schemas' export default new IntegrationDefinition({ name: 'zoho', - version: '3.1.4', + version: '4.0.0', title: 'Zoho', readme: 'hub.md', icon: 'icon.svg', description: 'Integrate your Botpress chatbot with Zoho CRM to manage customer interactions. Add, update, and retrieve contacts, deals, orders, and appointments directly through your chatbot.', configuration: { - schema: z.object({ - clientId: z.string().title('Client ID').describe('Your Zoho Client ID'), - clientSecret: z.string().title('Client Secret').describe('Your Zoho Client Secret'), - refreshToken: z.string().title('Refresh Token').describe('Your Zoho Refresh Token'), - dataCenter: z - .enum(['us', 'eu', 'in', 'au', 'cn', 'jp', 'ca']) - .title('Data Center Region') - .describe('Zoho Data Center Region'), - }), + identifier: { + linkTemplateScript: 'linkTemplate.vrl', + required: true, + }, + schema: z.object({}), + }, + configurations: { + manual: { + title: 'Manual configuration', + description: 'Configure manually with Zoho OAuth client credentials and a refresh token', + schema: z.object({ + clientId: z.string().title('Client ID').describe('Your Zoho Client ID'), + clientSecret: z.string().title('Client Secret').describe('Your Zoho Client Secret'), + refreshToken: z.string().title('Refresh Token').describe('Your Zoho Refresh Token'), + dataCenter: z.enum(DATA_CENTERS).title('Data Center Region').describe('Zoho Data Center Region'), + }), + }, + }, + secrets: { + CLIENT_ID: { + description: 'The OAuth Client ID provided by Zoho.', + }, + CLIENT_SECRET: { + description: 'The OAuth Client Secret provided by Zoho.', + }, }, user: { tags: { @@ -67,9 +84,11 @@ export default new IntegrationDefinition({ states: { credentials: { type: 'integration', - schema: z.object({ - accessToken: z.string().title('Access Token').describe('Your Zoho Access Token'), - }), + schema: zohoCredentialsStateSchema, + }, + oauthWizard: { + type: 'integration', + schema: zohoOAuthWizardStateSchema, }, }, actions: { diff --git a/integrations/zoho/linkTemplate.vrl b/integrations/zoho/linkTemplate.vrl new file mode 100644 index 00000000000..23372049f7a --- /dev/null +++ b/integrations/zoho/linkTemplate.vrl @@ -0,0 +1,4 @@ +webhookId = to_string!(.webhookId) +webhookUrl = to_string!(.webhookUrl) + +"{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}" diff --git a/integrations/zoho/package.json b/integrations/zoho/package.json index 1d6e37457b7..2e180b9bea5 100644 --- a/integrations/zoho/package.json +++ b/integrations/zoho/package.json @@ -9,9 +9,11 @@ "private": true, "dependencies": { "@botpress/client": "workspace:*", + "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "axios": "^1.4.0", - "form-data": "^4.0.0" + "form-data": "^4.0.0", + "preact": "^10.26.6" }, "devDependencies": { "@botpress/cli": "workspace:*", diff --git a/integrations/zoho/src/actions/create-appointment.ts b/integrations/zoho/src/actions/create-appointment.ts index 334a393e401..c12539f8223 100644 --- a/integrations/zoho/src/actions/create-appointment.ts +++ b/integrations/zoho/src/actions/create-appointment.ts @@ -10,14 +10,7 @@ export const createAppointment: IntegrationProps['actions']['createAppointment'] }) => { const validatedInput = createAppointmentInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/delete-appointment.ts b/integrations/zoho/src/actions/delete-appointment.ts index 79de1cf2bed..0aa2a9defc1 100644 --- a/integrations/zoho/src/actions/delete-appointment.ts +++ b/integrations/zoho/src/actions/delete-appointment.ts @@ -10,14 +10,7 @@ export const deleteAppointment: IntegrationProps['actions']['deleteAppointment'] }) => { const validatedInput = deleteAppointmentInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/delete-record.ts b/integrations/zoho/src/actions/delete-record.ts index 34ea92c3fac..2664b5dfe20 100644 --- a/integrations/zoho/src/actions/delete-record.ts +++ b/integrations/zoho/src/actions/delete-record.ts @@ -5,14 +5,7 @@ import type { IntegrationProps } from '../misc/types' export const deleteRecord: IntegrationProps['actions']['deleteRecord'] = async ({ ctx, client, logger, input }) => { const validatedInput = deleteRecordInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/get-appointment-by-id.ts b/integrations/zoho/src/actions/get-appointment-by-id.ts index 0b0d925b589..f8d36206064 100644 --- a/integrations/zoho/src/actions/get-appointment-by-id.ts +++ b/integrations/zoho/src/actions/get-appointment-by-id.ts @@ -10,14 +10,7 @@ export const getAppointmentById: IntegrationProps['actions']['getAppointmentById }) => { const validatedInput = getAppointmentByIdInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/get-appointments.ts b/integrations/zoho/src/actions/get-appointments.ts index e8b80b188a4..2a259ccd911 100644 --- a/integrations/zoho/src/actions/get-appointments.ts +++ b/integrations/zoho/src/actions/get-appointments.ts @@ -11,14 +11,7 @@ export const getAppointments: IntegrationProps['actions']['getAppointments'] = a const validatedInput = getAppointmentsInputSchema.parse(input) const params = validatedInput.params ?? '{}' - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/get-file.ts b/integrations/zoho/src/actions/get-file.ts index 922391d4477..75ffe355def 100644 --- a/integrations/zoho/src/actions/get-file.ts +++ b/integrations/zoho/src/actions/get-file.ts @@ -5,14 +5,7 @@ import type { IntegrationProps } from '../misc/types' export const getFile: IntegrationProps['actions']['getFile'] = async ({ ctx, client, logger, input }) => { const validatedInput = getFileInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/get-organization-details.ts b/integrations/zoho/src/actions/get-organization-details.ts index 99b43858402..011610a8aa2 100644 --- a/integrations/zoho/src/actions/get-organization-details.ts +++ b/integrations/zoho/src/actions/get-organization-details.ts @@ -6,14 +6,7 @@ export const getOrganizationDetails: IntegrationProps['actions']['getOrganizatio client, logger, }) => { - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) try { const result = await zohoClient.getOrganizationDetails() diff --git a/integrations/zoho/src/actions/get-record-by-id.ts b/integrations/zoho/src/actions/get-record-by-id.ts index f93a7ac577f..97788388b98 100644 --- a/integrations/zoho/src/actions/get-record-by-id.ts +++ b/integrations/zoho/src/actions/get-record-by-id.ts @@ -5,14 +5,7 @@ import type { IntegrationProps } from '../misc/types' export const getRecordById: IntegrationProps['actions']['getRecordById'] = async ({ ctx, client, logger, input }) => { const validatedInput = getRecordByIdInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/get-records.ts b/integrations/zoho/src/actions/get-records.ts index abf9918d6e2..0dee8fc41f7 100644 --- a/integrations/zoho/src/actions/get-records.ts +++ b/integrations/zoho/src/actions/get-records.ts @@ -6,14 +6,7 @@ export const getRecords: IntegrationProps['actions']['getRecords'] = async ({ ct const validatedInput = getRecordsInputSchema.parse(input) const params = validatedInput.params ?? '{}' // Default to empty JSON if no params provided - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - Module: ${validatedInput.module}, Params: ${params}`) diff --git a/integrations/zoho/src/actions/get-users.ts b/integrations/zoho/src/actions/get-users.ts index e0a79008de8..5263332a698 100644 --- a/integrations/zoho/src/actions/get-users.ts +++ b/integrations/zoho/src/actions/get-users.ts @@ -6,14 +6,7 @@ export const getUsers: IntegrationProps['actions']['getUsers'] = async ({ ctx, c const validatedInput = getUsersInputSchema.parse(input) const params = validatedInput.params ?? '{}' // Ensure params is always a valid JSON string - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - Params: ${params}`) diff --git a/integrations/zoho/src/actions/insert-record.ts b/integrations/zoho/src/actions/insert-record.ts index abade8f2f09..e3f7e5d01c4 100644 --- a/integrations/zoho/src/actions/insert-record.ts +++ b/integrations/zoho/src/actions/insert-record.ts @@ -4,14 +4,7 @@ import type { IntegrationProps } from '../misc/types' export const insertRecord: IntegrationProps['actions']['insertRecord'] = async ({ ctx, client, logger, input }) => { const validatedInput = insertRecordInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/make-api-call.ts b/integrations/zoho/src/actions/make-api-call.ts index 5a1e68a246e..7fccf4f599d 100644 --- a/integrations/zoho/src/actions/make-api-call.ts +++ b/integrations/zoho/src/actions/make-api-call.ts @@ -5,14 +5,7 @@ import type { IntegrationProps } from '../misc/types' export const makeApiCall: IntegrationProps['actions']['makeApiCall'] = async ({ ctx, client, logger, input }) => { const validatedInput = makeApiCallInputSchema.parse(input) const params = validatedInput.params ?? '{}' // Default to empty JSON if no params provided - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) try { const result = await zohoClient.makeApiCall( diff --git a/integrations/zoho/src/actions/search-records.ts b/integrations/zoho/src/actions/search-records.ts index 663da1e6a2f..5503f9e286e 100644 --- a/integrations/zoho/src/actions/search-records.ts +++ b/integrations/zoho/src/actions/search-records.ts @@ -5,14 +5,7 @@ import type { IntegrationProps } from '../misc/types' export const searchRecords: IntegrationProps['actions']['searchRecords'] = async ({ ctx, client, logger, input }) => { const validatedInput = searchRecordsInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/send-email.ts b/integrations/zoho/src/actions/send-email.ts index 9f553355c96..944bce86746 100644 --- a/integrations/zoho/src/actions/send-email.ts +++ b/integrations/zoho/src/actions/send-email.ts @@ -5,14 +5,7 @@ import type { IntegrationProps } from '../misc/types' export const sendMail: IntegrationProps['actions']['sendMail'] = async ({ ctx, client, logger, input }) => { const validatedInput = sendMailInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/update-appointment.ts b/integrations/zoho/src/actions/update-appointment.ts index 1154c83b65e..a7e1f19eda9 100644 --- a/integrations/zoho/src/actions/update-appointment.ts +++ b/integrations/zoho/src/actions/update-appointment.ts @@ -10,14 +10,7 @@ export const updateAppointment: IntegrationProps['actions']['updateAppointment'] }) => { const validatedInput = updateAppointmentInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/update-record.ts b/integrations/zoho/src/actions/update-record.ts index 20303ba6713..db8518b71eb 100644 --- a/integrations/zoho/src/actions/update-record.ts +++ b/integrations/zoho/src/actions/update-record.ts @@ -4,14 +4,7 @@ import type { IntegrationProps } from '../misc/types' export const updateRecord: IntegrationProps['actions']['updateRecord'] = async ({ ctx, client, logger, input }) => { const validatedInput = updateRecordInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/actions/upload-file.ts b/integrations/zoho/src/actions/upload-file.ts index d54ba19fa62..6a8acd6c2ca 100644 --- a/integrations/zoho/src/actions/upload-file.ts +++ b/integrations/zoho/src/actions/upload-file.ts @@ -4,14 +4,7 @@ import type { IntegrationProps } from '../misc/types' export const uploadFile: IntegrationProps['actions']['uploadFile'] = async ({ ctx, client, logger, input }) => { const validatedInput = uploadFileInputSchema.parse(input) - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`) diff --git a/integrations/zoho/src/client.ts b/integrations/zoho/src/client.ts index 6971866da87..4d578d45685 100644 --- a/integrations/zoho/src/client.ts +++ b/integrations/zoho/src/client.ts @@ -1,52 +1,87 @@ import { IntegrationLogger } from '@botpress/sdk' -import axios, { AxiosError } from 'axios' +import axios from 'axios' import FormData from 'form-data' +import { DataCenter, getZohoApiBaseUrl, getZohoAuthUrl, isDataCenter } from './misc/data-centers' import * as bp from '.botpress' const logger = new IntegrationLogger() +const OAUTH_CLIENT_ID = bp.secrets.CLIENT_ID +const OAUTH_CLIENT_SECRET = bp.secrets.CLIENT_SECRET + +// Retry once after refreshing the access token. If Zoho still returns 401, +// the credentials are likely revoked, mis-scoped, or tied to the wrong data center. +const MAX_AUTH_RETRIES = 1 + +type AuthMode = 'oauth' | 'manual' +type StoredCredentials = { + accessToken: string + refreshToken?: string + dataCenter?: DataCenter + apiDomain?: string + expiresAt?: number +} +type LegacyConfiguration = { + clientId: string + clientSecret: string + refreshToken: string + dataCenter: DataCenter +} + +const _getErrorMessage = (error: unknown): string => { + if (axios.isAxiosError(error)) { + return error.response?.data?.message ?? error.message + } + + return error instanceof Error ? error.message : 'Unknown error' +} + +const _getErrorLogData = (error: unknown): unknown => { + if (axios.isAxiosError(error)) { + return error.response?.data ?? error.message + } + + return error instanceof Error ? error.message : error +} + +const _getLegacyConfiguration = (configuration: unknown): LegacyConfiguration | null => { + if (!configuration || typeof configuration !== 'object') { + return null + } + + const maybeConfiguration = configuration as Record + const { clientId, clientSecret, refreshToken, dataCenter } = maybeConfiguration + if ( + typeof clientId !== 'string' || + typeof clientSecret !== 'string' || + typeof refreshToken !== 'string' || + typeof dataCenter !== 'string' || + !isDataCenter(dataCenter) + ) { + return null + } -// Define a Map for Zoho Data Centers -const zohoAuthUrls = new Map([ - ['us', 'https://accounts.zoho.com'], - ['eu', 'https://accounts.zoho.eu'], - ['in', 'https://accounts.zoho.in'], - ['au', 'https://accounts.zoho.com.au'], - ['cn', 'https://accounts.zoho.com.cn'], - ['jp', 'https://accounts.zoho.jp'], - ['ca', 'https://accounts.zohocloud.ca'], -]) - -const zohoDataCenterTLDs = new Map([ - ['us', 'com'], - ['eu', 'eu'], - ['in', 'in'], - ['au', 'com.au'], - ['cn', 'com.cn'], - ['jp', 'jp'], - ['ca', 'ca'], -]) - -// Function to get the Zoho Auth URL -const getZohoAuthUrl = (region: string): string => zohoAuthUrls.get(region) ?? 'https://accounts.zoho.com' - -const getZohoDataCenterTLD = (region: string): string => zohoDataCenterTLDs.get(region) ?? 'com' + return { clientId, clientSecret, refreshToken, dataCenter } +} export class ZohoApi { private _refreshToken: string private _clientId: string private _clientSecret: string - private _dataCenter: string + private _dataCenter: DataCenter private _baseUrl: string private _ctx: bp.Context private _bpClient: bp.Client + private _authMode: AuthMode public constructor( refreshToken: string, clientId: string, clientSecret: string, - dataCenter: string, + dataCenter: DataCenter, ctx: bp.Context, - bpClient: bp.Client + bpClient: bp.Client, + authMode: AuthMode = 'manual', + apiDomain?: string ) { this._refreshToken = refreshToken this._clientId = clientId @@ -54,11 +89,12 @@ export class ZohoApi { this._dataCenter = dataCenter this._ctx = ctx this._bpClient = bpClient - this._baseUrl = `https://www.zohoapis.${getZohoDataCenterTLD(dataCenter)}` + this._authMode = authMode + this._baseUrl = apiDomain ?? getZohoApiBaseUrl(dataCenter) } /** Retrieves stored credentials from Botpress state */ - private async _getStoredCredentials(): Promise<{ accessToken: string } | null> { + private async _getStoredCredentials(): Promise { try { const { state } = await this._bpClient.getState({ id: this._ctx.integrationId, @@ -73,6 +109,10 @@ export class ZohoApi { return { accessToken: state.payload.accessToken, + refreshToken: state.payload.refreshToken, + dataCenter: state.payload.dataCenter, + apiDomain: state.payload.apiDomain, + expiresAt: state.payload.expiresAt, } } catch (error) { logger.forBot().error('Error retrieving credentials from state:', error) @@ -84,7 +124,8 @@ export class ZohoApi { endpoint: string, method: string = 'GET', data: any = null, - params: any = {} + params: any = {}, + retryCount: number = 0 ): Promise { try { const creds = await this._getStoredCredentials() @@ -97,7 +138,6 @@ export class ZohoApi { Authorization: `Bearer ${creds.accessToken}`, Accept: 'application/json', } - logger.forBot().info('accessToken', creds.accessToken) if (method !== 'GET' && method !== 'DELETE') { headers['Content-Type'] = 'application/json' } @@ -113,18 +153,18 @@ export class ZohoApi { }) return { success: true, message: 'Request successful', data: response.data } - } catch (error: any) { - if (error.response?.status === 401) { + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response?.status === 401 && retryCount < MAX_AUTH_RETRIES) { logger.forBot().warn('Access token expired. Refreshing...', error) await this.refreshAccessToken() - return this._makeRequest(endpoint, method, data, params) + return this._makeRequest(endpoint, method, data, params, retryCount + 1) } - logger.forBot().error(`Error in ${method} ${endpoint}:`, error.response?.data || error.message) - return { success: false, message: error.response?.data?.message || error.message, data: null } + logger.forBot().error(`Error in ${method} ${endpoint}:`, _getErrorLogData(error)) + return { success: false, message: _getErrorMessage(error), data: null } } } - private async _makeFileUploadRequest(endpoint: string, formData: FormData): Promise { + private async _makeFileUploadRequest(endpoint: string, formData: FormData, retryCount: number = 0): Promise { try { const creds = await this._getStoredCredentials() if (!creds) { @@ -142,14 +182,14 @@ export class ZohoApi { const response = await axios.post(`${this._baseUrl}${endpoint}`, formData, { headers }) return { success: true, message: 'File uploaded successfully', data: response.data } - } catch (error: any) { - if (error.response?.status === 401) { + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response?.status === 401 && retryCount < MAX_AUTH_RETRIES) { logger.forBot().warn('Access token expired. Refreshing...', error) await this.refreshAccessToken() - return this._makeFileUploadRequest(endpoint, formData) + return this._makeFileUploadRequest(endpoint, formData, retryCount + 1) } - logger.forBot().error(`Error in file upload ${endpoint}:`, error.response?.data || error.message) - return { success: false, message: error.response?.data?.message || error.message, data: null } + logger.forBot().error(`Error in file upload ${endpoint}:`, _getErrorLogData(error)) + return { success: false, message: _getErrorMessage(error), data: null } } } @@ -161,30 +201,34 @@ export class ZohoApi { requestData.append('refresh_token', this._refreshToken) requestData.append('grant_type', 'refresh_token') - const response = await axios.post( - `${getZohoAuthUrl(this._ctx.configuration.dataCenter)}/oauth/v2/token`, - requestData.toString(), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - } - ) + const response = await axios.post(`${getZohoAuthUrl(this._dataCenter)}/oauth/v2/token`, requestData.toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + + const currentCredentials = await this._getStoredCredentials() await this._bpClient.setState({ id: this._ctx.integrationId, type: 'integration', name: 'credentials', payload: { + ...currentCredentials, accessToken: response.data.access_token, + refreshToken: this._authMode === 'oauth' ? this._refreshToken : currentCredentials?.refreshToken, + dataCenter: this._authMode === 'oauth' ? this._dataCenter : currentCredentials?.dataCenter, + apiDomain: response.data.api_domain ?? currentCredentials?.apiDomain, + expiresAt: response.data.expires_in + ? Date.now() + response.data.expires_in * 1000 + : currentCredentials?.expiresAt, }, }) logger.forBot().info('Access token refreshed successfully.') } catch (error: unknown) { - const err = error as AxiosError - logger.forBot().error(err.response?.data) - logger.forBot().error('Error refreshing access token:', err.response?.data || err.message) + logger.forBot().error(_getErrorLogData(error)) + logger.forBot().error('Error refreshing access token:', _getErrorLogData(error)) throw new Error('Authentication error. Please reauthorize the integration.') } } @@ -246,8 +290,6 @@ export class ZohoApi { } public async uploadFile(fileUrl: string) { - logger.forBot().error('FILE URL SHARK: ', fileUrl) - try { const file = await this.downloadFileBuffer(fileUrl) @@ -298,13 +340,40 @@ export class ZohoApi { } } -export const getClient = ( - refreshToken: string, - clientId: string, - clientSecret: string, - dataCenter: string, - ctx: bp.Context, - bpClient: bp.Client -) => { - return new ZohoApi(refreshToken, clientId, clientSecret, dataCenter, ctx, bpClient) +export const getClient = async (ctx: bp.Context, bpClient: bp.Client): Promise => { + const manualConfiguration = + ctx.configurationType === 'manual' ? ctx.configuration : _getLegacyConfiguration(ctx.configuration) + + if (manualConfiguration) { + return new ZohoApi( + manualConfiguration.refreshToken, + manualConfiguration.clientId, + manualConfiguration.clientSecret, + manualConfiguration.dataCenter, + ctx, + bpClient, + 'manual' + ) + } + + const { state } = await bpClient.getState({ + id: ctx.integrationId, + name: 'credentials', + type: 'integration', + }) + const credentials = state.payload + if (!credentials.refreshToken || !credentials.dataCenter) { + throw new Error('Zoho OAuth credentials not found. Please reconnect the integration.') + } + + return new ZohoApi( + credentials.refreshToken, + OAUTH_CLIENT_ID, + OAUTH_CLIENT_SECRET, + credentials.dataCenter, + ctx, + bpClient, + 'oauth', + credentials.apiDomain + ) } diff --git a/integrations/zoho/src/misc/data-centers.ts b/integrations/zoho/src/misc/data-centers.ts new file mode 100644 index 00000000000..451a52e36b1 --- /dev/null +++ b/integrations/zoho/src/misc/data-centers.ts @@ -0,0 +1,46 @@ +export const DATA_CENTERS = ['us', 'eu', 'in', 'au', 'cn', 'jp', 'ca'] as const + +export type DataCenter = (typeof DATA_CENTERS)[number] + +export const DATA_CENTER_LABELS: Record = { + us: 'US - accounts.zoho.com', + eu: 'EU - accounts.zoho.eu', + in: 'IN - accounts.zoho.in', + au: 'AU - accounts.zoho.com.au', + cn: 'CN - accounts.zoho.com.cn', + jp: 'JP - accounts.zoho.jp', + ca: 'CA - accounts.zohocloud.ca', +} + +const ZOHO_AUTH_URLS: Record = { + us: 'https://accounts.zoho.com', + eu: 'https://accounts.zoho.eu', + in: 'https://accounts.zoho.in', + au: 'https://accounts.zoho.com.au', + cn: 'https://accounts.zoho.com.cn', + jp: 'https://accounts.zoho.jp', + ca: 'https://accounts.zohocloud.ca', +} + +const ZOHO_DATA_CENTER_TLDS: Record = { + us: 'com', + eu: 'eu', + in: 'in', + au: 'com.au', + cn: 'com.cn', + jp: 'jp', + ca: 'ca', +} + +export const DATA_CENTER_CHOICES: { label: string; value: DataCenter }[] = DATA_CENTERS.map((dataCenter) => ({ + label: DATA_CENTER_LABELS[dataCenter], + value: dataCenter, +})) + +export const isDataCenter = (value: string | undefined): value is DataCenter => + DATA_CENTERS.includes(value as DataCenter) + +export const getZohoAuthUrl = (dataCenter: DataCenter): string => ZOHO_AUTH_URLS[dataCenter] + +export const getZohoApiBaseUrl = (dataCenter: DataCenter): string => + `https://www.zohoapis.${ZOHO_DATA_CENTER_TLDS[dataCenter]}` diff --git a/integrations/zoho/src/misc/oauth-schemas.ts b/integrations/zoho/src/misc/oauth-schemas.ts new file mode 100644 index 00000000000..99877a3d655 --- /dev/null +++ b/integrations/zoho/src/misc/oauth-schemas.ts @@ -0,0 +1,24 @@ +import { z } from '@botpress/sdk' +import { DATA_CENTERS } from './data-centers' + +export const zohoTokenResponseSchema = z.object({ + access_token: z.string().min(1), + refresh_token: z.string().min(1), + api_domain: z.string().optional(), + expires_in: z.number().optional(), +}) + +export const zohoCredentialsStateSchema = z.object({ + accessToken: z.string().title('Access Token').describe('Your Zoho Access Token'), + refreshToken: z.string().optional().title('Refresh Token').describe('Your Zoho Refresh Token'), + dataCenter: z.enum(DATA_CENTERS).optional().title('Data Center Region').describe('Zoho Data Center Region'), + apiDomain: z.string().optional().title('API Domain').describe('Zoho API domain returned by OAuth'), + expiresAt: z.number().optional().title('Expiration Timestamp').describe('Access token expiration timestamp'), +}) + +export const zohoOAuthWizardStateSchema = z.object({ + dataCenter: z + .enum(DATA_CENTERS) + .title('Data Center Region') + .describe('Zoho Data Center Region selected during OAuth setup'), +}) diff --git a/integrations/zoho/src/oauth-wizard/index.ts b/integrations/zoho/src/oauth-wizard/index.ts new file mode 100644 index 00000000000..cf886eb0694 --- /dev/null +++ b/integrations/zoho/src/oauth-wizard/index.ts @@ -0,0 +1,17 @@ +import { generateRedirection } from '@botpress/common/src/html-dialogs' +import { getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import * as wizard from './wizard' +import * as bp from '.botpress' + +export const oauthWizardHandler: bp.IntegrationProps['handler'] = async (props) => { + const { logger } = props + + try { + return await wizard.handler(props) + } catch (thrown: unknown) { + const error = thrown instanceof Error ? thrown : Error(String(thrown)) + const message = `OAuth wizard error: ${error.message}` + logger.forBot().error(message) + return generateRedirection(getInterstitialUrl(false, message)) + } +} diff --git a/integrations/zoho/src/oauth-wizard/wizard.ts b/integrations/zoho/src/oauth-wizard/wizard.ts new file mode 100644 index 00000000000..c40c7bc6909 --- /dev/null +++ b/integrations/zoho/src/oauth-wizard/wizard.ts @@ -0,0 +1,146 @@ +import * as oauthWizard from '@botpress/common/src/oauth-wizard' +import axios from 'axios' +import { DATA_CENTER_CHOICES, getZohoAuthUrl, isDataCenter } from '../misc/data-centers' +import { zohoTokenResponseSchema } from '../misc/oauth-schemas' +import * as bp from '.botpress' + +type WizardHandler = oauthWizard.WizardStepHandler + +const ZOHO_SCOPES = [ + 'ZohoCRM.modules.ALL', + 'ZohoCRM.org.ALL', + 'ZohoCRM.users.ALL', + 'ZohoCRM.settings.ALL', + 'ZohoCRM.send_mail.all.CREATE', + 'ZohoCRM.files.CREATE', + 'ZohoCRM.files.READ', +] + +const _getOAuthRedirectUri = () => oauthWizard.getWizardStepUrl('oauth-callback').toString() + +export const handler = async (props: bp.HandlerProps) => { + return await new oauthWizard.OAuthWizardBuilder(props) + .addStep({ id: 'start', handler: _startStep }) + .addStep({ id: 'oauth-redirect', handler: _oauthRedirectStep }) + .addStep({ id: 'oauth-callback', handler: _oauthCallbackStep }) + .build() + .handleRequest() +} + +const _startStep: WizardHandler = ({ responses }) => { + return responses.displayChoices({ + pageTitle: 'Connect Zoho CRM', + htmlOrMarkdownPageContents: 'Select the Zoho data center for the account you want to connect.', + choices: DATA_CENTER_CHOICES, + nextStepId: 'oauth-redirect', + defaultValues: ['us'], + }) +} + +const _oauthRedirectStep: WizardHandler = async ({ selectedChoice, responses, ctx, client }) => { + if (!isDataCenter(selectedChoice)) { + return responses.endWizard({ + success: false, + errorMessage: 'Please select a valid Zoho data center.', + }) + } + + await client.setState({ + id: ctx.integrationId, + type: 'integration', + name: 'oauthWizard', + payload: { + dataCenter: selectedChoice, + }, + }) + + const params = new URLSearchParams({ + scope: ZOHO_SCOPES.join(','), + client_id: bp.secrets.CLIENT_ID, + response_type: 'code', + access_type: 'offline', + prompt: 'consent', + redirect_uri: _getOAuthRedirectUri(), + state: ctx.webhookId, + }) + + return responses.redirectToExternalUrl(`${getZohoAuthUrl(selectedChoice)}/oauth/v2/auth?${params.toString()}`) +} + +const _oauthCallbackStep: WizardHandler = async ({ query, responses, client, ctx, logger }) => { + const error = query.get('error') + if (error) { + const description = query.get('error_description') ?? '' + return responses.endWizard({ success: false, errorMessage: `OAuth error: ${error} - ${description}` }) + } + + const code = query.get('code') + if (!code) { + return responses.endWizard({ success: false, errorMessage: 'Authorization code not present in OAuth callback.' }) + } + + const returnedState = query.get('state') + if (returnedState !== ctx.webhookId) { + logger.forBot().warn('Invalid Zoho OAuth state parameter received.') + return responses.endWizard({ success: false, errorMessage: 'Invalid OAuth state parameter.' }) + } + + const { state } = await client.getState({ + id: ctx.integrationId, + type: 'integration', + name: 'oauthWizard', + }) + const dataCenter = state.payload.dataCenter + if (!isDataCenter(dataCenter)) { + return responses.endWizard({ success: false, errorMessage: 'Zoho data center not found. Please reconnect.' }) + } + + const requestData = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: bp.secrets.CLIENT_ID, + client_secret: bp.secrets.CLIENT_SECRET, + redirect_uri: _getOAuthRedirectUri(), + code, + }) + + let response + try { + response = await axios.post(`${getZohoAuthUrl(dataCenter)}/oauth/v2/token`, requestData.toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + } catch (error: unknown) { + logger.forBot().error('Zoho OAuth token exchange failed.', axios.isAxiosError(error) ? error.response?.data : error) + return responses.endWizard({ + success: false, + errorMessage: 'Zoho could not exchange the authorization code. Please reconnect and try again.', + }) + } + + const tokenResponse = zohoTokenResponseSchema.safeParse(response.data) + if (!tokenResponse.success) { + logger.forBot().warn('Zoho OAuth token response failed validation.', tokenResponse.error.message) + return responses.endWizard({ + success: false, + errorMessage: 'Zoho returned an invalid OAuth token response. Please reconnect and grant consent.', + }) + } + + await client.setState({ + id: ctx.integrationId, + type: 'integration', + name: 'credentials', + payload: { + accessToken: tokenResponse.data.access_token, + refreshToken: tokenResponse.data.refresh_token, + dataCenter, + apiDomain: tokenResponse.data.api_domain, + expiresAt: tokenResponse.data.expires_in ? Date.now() + tokenResponse.data.expires_in * 1000 : undefined, + }, + }) + + await client.configureIntegration({ identifier: ctx.webhookId }) + + return responses.endWizard({ success: true }) +} diff --git a/integrations/zoho/src/setup/handler.ts b/integrations/zoho/src/setup/handler.ts index b8e619b1587..000a09d225c 100644 --- a/integrations/zoho/src/setup/handler.ts +++ b/integrations/zoho/src/setup/handler.ts @@ -1,9 +1,18 @@ +import { isOAuthWizardUrl } from '@botpress/common/src/oauth-wizard' import type { Handler } from '../misc/types' +import { oauthWizardHandler } from '../oauth-wizard' -export const handler: Handler = async ({ req }) => { - if (!req.body) { - console.warn('Handler received an empty body') - return {} +export const handler: Handler = async (props) => { + if (isOAuthWizardUrl(props.req.path)) { + return await oauthWizardHandler(props) + } + + props.logger.forBot().warn('Received request for an invalid OAuth wizard endpoint.', { + path: props.req.path, + }) + + return { + status: 404, + body: 'Invalid OAuth wizard endpoint', } - return {} } diff --git a/integrations/zoho/src/setup/register.ts b/integrations/zoho/src/setup/register.ts index 84854db97ff..839962a3dde 100644 --- a/integrations/zoho/src/setup/register.ts +++ b/integrations/zoho/src/setup/register.ts @@ -4,14 +4,7 @@ import type { RegisterFunction } from '../misc/types' export const register: RegisterFunction = async ({ ctx, client, logger }) => { try { - const zohoClient = getClient( - ctx.configuration.refreshToken, - ctx.configuration.clientId, - ctx.configuration.clientSecret, - ctx.configuration.dataCenter, - ctx, - client - ) + const zohoClient = await getClient(ctx, client) await zohoClient.refreshAccessToken() diff --git a/integrations/zoho/tsconfig.json b/integrations/zoho/tsconfig.json index d46abc5b88f..4fc5b7c714f 100644 --- a/integrations/zoho/tsconfig.json +++ b/integrations/zoho/tsconfig.json @@ -1,8 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact", "paths": { "*": ["./*"] }, - "outDir": "dist" + "outDir": "dist", + "types": ["preact"] }, "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"] } diff --git a/package.json b/package.json index 3b8af5915fb..9a5fcb6edf4 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "typescript-eslint": "^8.42.0", "vitest": "^2.1.4" }, - "packageManager": "pnpm@10.12.4", + "packageManager": "pnpm@10.29.3", "pnpm": { "patchedDependencies": { "source-map-js@1.2.1": "patches/source-map-js@1.2.1.patch" diff --git a/packages/chat-client/package.json b/packages/chat-client/package.json index e27060c566b..1520f8d2714 100644 --- a/packages/chat-client/package.json +++ b/packages/chat-client/package.json @@ -49,5 +49,5 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/packages/cli/package.json b/packages/cli/package.json index 206a7380090..961c168aa61 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -73,5 +73,5 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/packages/client/package.json b/packages/client/package.json index 89e98f8401a..e60fc35f0a4 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -40,5 +40,5 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/packages/cognitive/package.json b/packages/cognitive/package.json index b87d895a8f2..12b73d73b8a 100644 --- a/packages/cognitive/package.json +++ b/packages/cognitive/package.json @@ -49,5 +49,5 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/packages/llmz/package.json b/packages/llmz/package.json index c115f4099de..f74d780c4cb 100644 --- a/packages/llmz/package.json +++ b/packages/llmz/package.json @@ -95,5 +95,5 @@ "optional": false } }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index fa89d0de8f3..53c374c07e8 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -27,8 +27,7 @@ "devDependencies": { "@types/semver": "^7.3.11", "esbuild": "^0.25.10", - "esbuild-plugin-polyfill-node": "^0.3.0", - "tsup": "^8.0.2" + "esbuild-plugin-polyfill-node": "^0.3.0" }, "peerDependencies": { "@bpinternal/zui": "^2.3.0", @@ -42,5 +41,5 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/packages/vai/package.json b/packages/vai/package.json index bc238e5191c..fb667597dc8 100644 --- a/packages/vai/package.json +++ b/packages/vai/package.json @@ -49,5 +49,5 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/packages/zai/package.json b/packages/zai/package.json index a50ecbb3581..6fdc8e178f8 100644 --- a/packages/zai/package.json +++ b/packages/zai/package.json @@ -62,5 +62,5 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/packages/zui/package.json b/packages/zui/package.json index f4dd5e82160..51619ea3619 100644 --- a/packages/zui/package.json +++ b/packages/zui/package.json @@ -36,5 +36,5 @@ "engines": { "node": ">=18.0.0" }, - "packageManager": "pnpm@10.12.4" + "packageManager": "pnpm@10.29.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e936a7d590..af962abcc06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2506,6 +2506,9 @@ importers: '@botpress/client': specifier: workspace:* version: link:../../packages/client + '@botpress/common': + specifier: workspace:* + version: link:../../packages/common '@botpress/sdk': specifier: workspace:* version: link:../../packages/sdk @@ -2515,6 +2518,9 @@ importers: form-data: specifier: ^4.0.0 version: 4.0.0 + preact: + specifier: ^10.26.6 + version: 10.26.6 devDependencies: '@botpress/cli': specifier: workspace:* @@ -3260,9 +3266,6 @@ importers: esbuild-plugin-polyfill-node: specifier: ^0.3.0 version: 0.3.0(esbuild@0.25.10) - tsup: - specifier: ^8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3) packages/sdk-addons: dependencies: @@ -4390,6 +4393,10 @@ packages: '@bpinternal/yargs-extra@0.0.3': resolution: {integrity: sha512-e/unlq0LX4CJUv1jGOv1UgwB/h2M0NCXnwD4lEw496GpkQikO668RS+BBlRhkqdGfZmvKDkXZZ96xJCn+i6Ymg==} + '@bpinternal/zui@2.3.0': + resolution: {integrity: sha512-U7LJnejsjB4tyzozP4ZuW2gs5CRqRtl4d1yAys56x0NDIlbxR5xF4lSSfSewLg9+gn0seyxDVRjZYrfMd2MbTg==} + engines: {node: '>=18.0.0'} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -14812,6 +14819,8 @@ snapshots: yargs: 17.7.2 yn: 4.0.0 + '@bpinternal/zui@2.3.0': {} + '@colors/colors@1.6.0': {} '@cspotcode/source-map-support@0.8.1': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 549740d6418..c1a920c5fb8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,9 @@ packages: - interfaces/* - bots/* - plugins/* + +minimumReleaseAge: 10080 # 7 days + +minimumReleaseAgeExclude: + - '@bpinternal/*' + - '@botpress/*'