From c0ed2de912d878e6dee731889ddcae324aeb1bef Mon Sep 17 00:00:00 2001 From: Mathieu Faucher <99497774+Math-Fauch@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:59:56 -0500 Subject: [PATCH 1/3] feat(integrations/dropbox): Add OAuth support (#14919) Co-authored-by: quu-ack --- .../dropbox/definitions/configuration.ts | 29 ++-- integrations/dropbox/definitions/secrets.ts | 2 + .../dropbox/integration.definition.ts | 5 +- integrations/dropbox/linkTemplate.vrl | 4 + .../dropbox/src/dropbox-api/dropbox-client.ts | 13 +- .../dropbox/src/dropbox-api/oauth-client.ts | 50 +++++-- integrations/dropbox/src/setup.ts | 100 +++++++++++--- .../src/webhook-events/handler-dispatcher.ts | 9 +- .../dropbox/src/webhook-events/oauth/index.ts | 23 ++++ .../src/webhook-events/oauth/wizard.ts | 125 ++++++++++++++++++ integrations/dropbox/tsconfig.json | 2 + 11 files changed, 318 insertions(+), 44 deletions(-) create mode 100644 integrations/dropbox/linkTemplate.vrl create mode 100644 integrations/dropbox/src/webhook-events/oauth/index.ts create mode 100644 integrations/dropbox/src/webhook-events/oauth/wizard.ts diff --git a/integrations/dropbox/definitions/configuration.ts b/integrations/dropbox/definitions/configuration.ts index b3c47131a2f..1d2357e7738 100644 --- a/integrations/dropbox/definitions/configuration.ts +++ b/integrations/dropbox/definitions/configuration.ts @@ -2,13 +2,22 @@ import { z } from '@botpress/sdk' import * as sdk from '@botpress/sdk' export const configuration = { - schema: z.object({ - clientId: z.string().title('App Key').describe('Available in the App Console on Dropbox'), - clientSecret: z.string().title('App Secret').describe('Available in the App Console on Dropbox').secret(), - authorizationCode: z - .string() - .title('Access Code') - .describe('Obtained by navigating to the authorization URL') - .secret(), - }), -} as const satisfies sdk.IntegrationDefinitionProps['configuration'] + identifier: { linkTemplateScript: 'linkTemplate.vrl', required: false }, + schema: z.object({}), +} satisfies sdk.IntegrationDefinitionProps['configuration'] + +export const configurations = { + manual: { + title: 'Manual Configuration', + description: 'Configure the Dropbox integration manually using your own app.', + schema: z.object({ + clientId: z.string().title('App Key').describe('Available in the App Console on Dropbox'), + clientSecret: z.string().title('App Secret').describe('Available in the App Console on Dropbox').secret(), + authorizationCode: z + .string() + .title('Access Code') + .describe('Obtained by navigating to the authorization URL') + .secret(), + }), + }, +} satisfies sdk.IntegrationDefinitionProps['configurations'] diff --git a/integrations/dropbox/definitions/secrets.ts b/integrations/dropbox/definitions/secrets.ts index decc5c441a6..5ce18adccf6 100644 --- a/integrations/dropbox/definitions/secrets.ts +++ b/integrations/dropbox/definitions/secrets.ts @@ -3,4 +3,6 @@ import { sentry as sentryHelpers } from '@botpress/sdk-addons' export const secrets = { ...sentryHelpers.COMMON_SECRET_NAMES, + APP_KEY: { description: 'Dropbox App Key' }, + APP_SECRET: { description: 'Dropbox App Secret' }, } as const satisfies sdk.IntegrationDefinitionProps['secrets'] diff --git a/integrations/dropbox/integration.definition.ts b/integrations/dropbox/integration.definition.ts index 76e3931dadb..bb446a7f40c 100644 --- a/integrations/dropbox/integration.definition.ts +++ b/integrations/dropbox/integration.definition.ts @@ -1,15 +1,16 @@ import * as sdk from '@botpress/sdk' import filesReadonly from './bp_modules/files-readonly' -import { actions, configuration, entities, secrets, states } from './definitions' +import { actions, configuration, configurations, entities, secrets, states } from './definitions' export default new sdk.IntegrationDefinition({ name: 'dropbox', title: 'Dropbox', - version: '1.2.2', + version: '2.0.0', description: 'Manage your files and folders effortlessly.', readme: 'hub.md', icon: 'icon.svg', configuration, + configurations, actions, entities, secrets, diff --git a/integrations/dropbox/linkTemplate.vrl b/integrations/dropbox/linkTemplate.vrl new file mode 100644 index 00000000000..cbebd3ea224 --- /dev/null +++ b/integrations/dropbox/linkTemplate.vrl @@ -0,0 +1,4 @@ +webhookId = to_string!(.webhookId) +webhookUrl = to_string!(.webhookUrl) + +"{{ webhookUrl }}/oauth/wizard/start-confirm?state={{ webhookId }}" diff --git a/integrations/dropbox/src/dropbox-api/dropbox-client.ts b/integrations/dropbox/src/dropbox-api/dropbox-client.ts index dcab795a640..8e827b5e46d 100644 --- a/integrations/dropbox/src/dropbox-api/dropbox-client.ts +++ b/integrations/dropbox/src/dropbox-api/dropbox-client.ts @@ -3,7 +3,7 @@ import { Dropbox } from 'dropbox' import { File as FileEntity, Folder as FolderEntity, Deleted as DeletedEntity } from '../../definitions' import { handleErrorsDecorator as handleErrors } from './error-handling' import { ActionInput, RequestMapping, ResponseMapping } from './mapping' -import { DropboxOAuthClient } from './oauth-client' +import { DropboxOAuthClient, getAuthorizationCode, getOAuthClientId, getOAuthClientSecret } from './oauth-client' import * as bp from '.botpress' type File = FileEntity.InferredType @@ -27,8 +27,8 @@ export class DropboxClient { return new DropboxClient({ accessToken, - clientId: ctx.configuration.clientId, - clientSecret: ctx.configuration.clientSecret, + clientId: getOAuthClientId({ ctx }), + clientSecret: getOAuthClientSecret({ ctx }), accountId, }) } @@ -38,8 +38,13 @@ export class DropboxClient { } public static async processAuthorizationCode(props: { client: bp.Client; ctx: bp.Context }): Promise { + const authorizationCode = getAuthorizationCode({ ctx: props.ctx }) + if (!authorizationCode) { + throw new Error('Authorization code is required') + } const oauthClient = new DropboxOAuthClient(props) - await oauthClient.processAuthorizationCode(props.ctx.configuration.authorizationCode) + // For manual configuration (no redirect_uri used), pass empty string + await oauthClient.processAuthorizationCode(authorizationCode, '') } @handleErrors('Failed to validate Dropbox authentication') diff --git a/integrations/dropbox/src/dropbox-api/oauth-client.ts b/integrations/dropbox/src/dropbox-api/oauth-client.ts index 6be1a398b33..80099ff6465 100644 --- a/integrations/dropbox/src/dropbox-api/oauth-client.ts +++ b/integrations/dropbox/src/dropbox-api/oauth-client.ts @@ -3,6 +3,19 @@ import { DropboxAuth } from 'dropbox' import { handleErrorsDecorator as handleErrors } from './error-handling' import * as bp from '.botpress' +export const getOAuthClientId = ({ ctx }: { ctx: bp.Context }) => + ctx.configurationType === 'manual' ? ctx.configuration.clientId : bp.secrets.APP_KEY + +export const getOAuthClientSecret = ({ ctx }: { ctx: bp.Context }) => + ctx.configurationType === 'manual' ? ctx.configuration.clientSecret : bp.secrets.APP_SECRET + +export const getAuthorizationCode = ({ ctx }: { ctx: bp.Context }): string | undefined => { + if (ctx.configurationType === 'manual') { + return ctx.configuration.authorizationCode + } + return undefined +} + export class DropboxOAuthClient { private readonly _client: bp.Client private readonly _ctx: bp.Context @@ -13,8 +26,8 @@ export class DropboxOAuthClient { this._ctx = ctx this._dropboxAuth = new DropboxAuth({ - clientId: ctx.configuration.clientId, - clientSecret: ctx.configuration.clientSecret, + clientId: getOAuthClientId({ ctx }), + clientSecret: getOAuthClientSecret({ ctx }), }) } @@ -26,8 +39,8 @@ export class DropboxOAuthClient { } @handleErrors('Failed to exchange authorization code. Please reconfigure the integration.') - public async processAuthorizationCode(authorizationCode: string): Promise { - const result = await this._exchangeAuthorizationCodeForRefreshToken(authorizationCode) + public async processAuthorizationCode(authorizationCode: string, redirectUri: string): Promise { + const result = await this._exchangeAuthorizationCodeForRefreshToken(authorizationCode, redirectUri) await this._client.setState({ id: this._ctx.integrationId, @@ -42,8 +55,8 @@ export class DropboxOAuthClient { }) } - private async _exchangeAuthorizationCodeForRefreshToken(authorizationCode: string) { - const response = await this._dropboxAuth.getAccessTokenFromCode('', authorizationCode) + private async _exchangeAuthorizationCodeForRefreshToken(authorizationCode: string, redirectUri: string) { + const response = await this._dropboxAuth.getAccessTokenFromCode(redirectUri, authorizationCode) // NOTE: DropboxAuth.getAccessTokenFromCode is not properly typed: the // response is not an empty object, but an object with the following properties: @@ -65,13 +78,26 @@ export class DropboxOAuthClient { @handleErrors('Failed to get authorization state. Please reconfigure the integration.') private async _getAuthState(): Promise { - const { state } = await this._client.getState({ - id: this._ctx.integrationId, - type: 'integration', - name: 'authorization', - }) + try { + const result = await this._client.getState({ + id: this._ctx.integrationId, + type: 'integration', + name: 'authorization', + }) - return state.payload + if (!result?.state?.payload) { + throw new Error('Authorization state not found. Please complete the OAuth wizard to configure the integration.') + } + + return result.state.payload + } catch (error) { + if (error instanceof Error && error.message.includes('not found')) { + throw error + } + throw new Error( + 'Failed to retrieve authorization state. Please complete the OAuth wizard to configure the integration.' + ) + } } @handleErrors('Failed to exchange refresh token for access token') diff --git a/integrations/dropbox/src/setup.ts b/integrations/dropbox/src/setup.ts index 625907600f1..45938e716d6 100644 --- a/integrations/dropbox/src/setup.ts +++ b/integrations/dropbox/src/setup.ts @@ -1,5 +1,6 @@ import * as sdk from '@botpress/sdk' import { DropboxClient } from './dropbox-api' +import { getAuthorizationCode } from './dropbox-api/oauth-client' import * as bp from '.botpress' type RegisterProps = Parameters[0] @@ -24,38 +25,107 @@ const _isAlreadyAuthenticatedWithSameCredentials = async (props: RegisterProps): return false } + if (props.ctx.configurationType !== 'manual') { + return true + } + + // For manual configurations, compare the authorization code from context with the one in state const { state } = await props.client.getState({ type: 'integration', id: props.ctx.integrationId, name: 'authorization', }) - return state.payload.authorizationCode === props.ctx.configuration.authorizationCode + const currentAuthorizationCode = getAuthorizationCode({ ctx: props.ctx }) + return state.payload.authorizationCode === currentAuthorizationCode } catch {} return false } -const _authenticate = async (props: RegisterProps): Promise => { - let authenticationSucceeded = false +const _authenticateWithRefreshToken = async (props: RegisterProps): Promise => { + const dropboxClient = await DropboxClient.create(props) + return dropboxClient.isProperlyAuthenticated() +} - try { - await DropboxClient.processAuthorizationCode(props) - const dropboxClient = await DropboxClient.create(props) - authenticationSucceeded = await dropboxClient.isProperlyAuthenticated() - } catch (thrown: unknown) { - console.error('Failed to authenticate with Dropbox', thrown) - authenticationSucceeded = false +const _authenticateWithAuthorizationCode = async (props: RegisterProps): Promise => { + const { logger } = props + await DropboxClient.processAuthorizationCode(props) + const dropboxClient = await DropboxClient.create(props) + const authenticated = await dropboxClient.isProperlyAuthenticated() + if (authenticated) { + logger.forBot().info('Successfully created Dropbox client from authorization code') } + return authenticated +} - if (!authenticationSucceeded) { - throw new sdk.RuntimeError( +const _authenticateManual = async (props: RegisterProps): Promise => { + const { ctx, logger } = props + const authorizationCode = getAuthorizationCode({ ctx }) + + if (!authorizationCode) { + logger.forBot().info('No authorization code provided, using existing refresh token from state') + return _authenticateWithRefreshToken(props).catch((err) => { + logger.forBot().warn({ err }, 'Failed to authenticate with existing refresh token') + return false + }) + } + + logger.forBot().info('Using authorization code from context') + let isAuthenticated = false + isAuthenticated = await _authenticateWithAuthorizationCode(props).catch((err) => { + logger?.forBot().warn({ err }, 'Failed to create Dropbox client from authorization code; falling back') + return false + }) + if (!isAuthenticated) { + isAuthenticated = await _authenticateWithAuthorizationCode(props).catch((err) => { + logger.forBot().error({ err }, 'Failed to authenticate with fallback') + return false + }) + } + return isAuthenticated +} + +const _authenticateOAuth = async (props: RegisterProps): Promise => { + const { logger } = props + logger.forBot().info('Using refresh token from state') + return _authenticateWithRefreshToken(props).catch((err) => { + logger.forBot().warn({ err }, 'Failed to authenticate with existing refresh token') + return false + }) +} + +const _getAuthFailureMessage = (configurationType: string): string => { + if (configurationType === 'manual') { + return ( 'Dropbox authentication failed. ' + - 'Please note that the Access Code is only valid for a few minutes. ' + - 'You may need to reauthorize your Dropbox application by navigating ' + - "to the authorization URL and update the integration's config accordingly." + 'Please note that the Access Code is only valid for a few minutes. ' + + 'You may need to reauthorize your Dropbox application by navigating ' + + "to the authorization URL and update the integration's config accordingly." ) } + return ( + 'Dropbox authentication failed. ' + + 'Please use the OAuth wizard to re-authenticate your Dropbox application. ' + + 'You can access the wizard through the integration configuration page.' + ) +} + +const _authenticate = async (props: RegisterProps): Promise => { + const { ctx, logger } = props + + if (ctx.configurationType !== 'manual') { + const authenticationSucceeded = await _authenticateOAuth(props) + if (!authenticationSucceeded) { + logger.forBot().info('No existing OAuth credentials found. Please complete the OAuth wizard to authenticate.') + } + return + } + + const authenticationSucceeded = await _authenticateManual(props) + if (!authenticationSucceeded) { + throw new sdk.RuntimeError(_getAuthFailureMessage(ctx.configurationType ?? '')) + } } const _saveRegistrationDate = async (props: RegisterProps): Promise => { diff --git a/integrations/dropbox/src/webhook-events/handler-dispatcher.ts b/integrations/dropbox/src/webhook-events/handler-dispatcher.ts index 69cf04d6b93..f5b5a7242b1 100644 --- a/integrations/dropbox/src/webhook-events/handler-dispatcher.ts +++ b/integrations/dropbox/src/webhook-events/handler-dispatcher.ts @@ -1,10 +1,16 @@ import * as sdk from '@botpress/sdk' import * as crypto from 'crypto' +import { getOAuthClientSecret } from '../dropbox-api/oauth-client' import { handleFileChangeEvent, isFileChangeNotification } from './handlers/file-change' import { isWebhookVerificationRequest, handleWebhookVerificationRequest } from './handlers/webhook-verification' +import { oauthCallbackHandler } from './oauth' import * as bp from '.botpress' export const handler: bp.IntegrationProps['handler'] = async (props) => { + if (props.req.path.startsWith('/oauth')) { + return await oauthCallbackHandler(props) + } + if (isWebhookVerificationRequest(props)) { return await handleWebhookVerificationRequest(props) } @@ -25,8 +31,9 @@ const _validatePayloadSignature = (props: bp.HandlerProps) => { throw new sdk.RuntimeError('Missing Dropbox signature in request headers') } + const clientSecret = getOAuthClientSecret({ ctx: props.ctx }) const bodySignatureFromBotpress = crypto - .createHmac('sha256', props.ctx.configuration.clientSecret) + .createHmac('sha256', clientSecret) .update(props.req.body ?? '') .digest('hex') diff --git a/integrations/dropbox/src/webhook-events/oauth/index.ts b/integrations/dropbox/src/webhook-events/oauth/index.ts new file mode 100644 index 00000000000..58909ec171e --- /dev/null +++ b/integrations/dropbox/src/webhook-events/oauth/index.ts @@ -0,0 +1,23 @@ +import { generateRedirection } from '@botpress/common/src/html-dialogs' +import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import * as wizard from './wizard' +import * as bp from '.botpress' + +export const oauthCallbackHandler: bp.IntegrationProps['handler'] = async (props) => { + const { req, logger } = props + if (!isOAuthWizardUrl(req.path)) { + return { + status: 404, + body: 'Invalid OAuth endpoint', + } + } + + try { + return await wizard.handler(props) + } catch (thrown: unknown) { + const error = thrown instanceof Error ? thrown : Error(String(thrown)) + const errorMessage = 'OAuth registration Error: ' + error.message + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) + } +} diff --git a/integrations/dropbox/src/webhook-events/oauth/wizard.ts b/integrations/dropbox/src/webhook-events/oauth/wizard.ts new file mode 100644 index 00000000000..53e92e40747 --- /dev/null +++ b/integrations/dropbox/src/webhook-events/oauth/wizard.ts @@ -0,0 +1,125 @@ +import * as oauthWizard from '@botpress/common/src/oauth-wizard' +import { DropboxOAuthClient, getOAuthClientId } from '../../dropbox-api/oauth-client' +import * as bp from '.botpress' + +type WizardHandler = oauthWizard.WizardStepHandler + +export const handler = async (props: bp.HandlerProps) => { + const wizard = new oauthWizard.OAuthWizardBuilder(props) + .addStep({ id: 'start-confirm', handler: _startHandler }) + .addStep({ id: 'redirect-to-dropbox', handler: _redirectToDropboxHandler }) + .addStep({ id: 'oauth-callback', handler: _oauthCallbackHandler }) + .addStep({ id: 'end', handler: _endHandler }) + .build() + + const response = await wizard.handleRequest() + return response +} + +const _startHandler: WizardHandler = (props) => { + const { responses } = props + return responses.displayButtons({ + pageTitle: 'Reset Configuration', + htmlOrMarkdownPageContents: + 'This wizard will reset your configuration, so the bot will stop working on Dropbox until a new configuration is put in place, continue?', + buttons: [ + { + action: 'navigate', + label: 'Yes', + navigateToStep: 'redirect-to-dropbox', + buttonType: 'primary', + }, + { + action: 'close', + label: 'No', + buttonType: 'secondary', + }, + ], + }) +} + +const _redirectToDropboxHandler: WizardHandler = async (props) => { + const { responses, ctx } = props + const clientId = getOAuthClientId({ ctx }) + + if (!clientId) { + return responses.endWizard({ + success: false, + errorMessage: 'Dropbox App Key (APP_KEY) is not configured. Please configure it in the integration settings.', + }) + } + + const redirectUri = _getOAuthRedirectUri() + + const dropboxAuthUrl = + 'https://www.dropbox.com/oauth2/authorize?' + + 'response_type=code' + + '&token_access_type=offline' + + `&client_id=${encodeURIComponent(clientId)}` + + `&redirect_uri=${encodeURIComponent(redirectUri)}` + + `&state=${encodeURIComponent(ctx.webhookId)}` + + return responses.redirectToExternalUrl(dropboxAuthUrl) +} + +const _getOAuthRedirectUri = (ctx?: bp.Context) => oauthWizard.getWizardStepUrl('oauth-callback', ctx).toString() + +const _oauthCallbackHandler: WizardHandler = async (props) => { + const { responses, query, client, ctx, logger } = props + + const oauthError = query.get('error') + const oauthErrorDescription = query.get('error_description') + if (oauthError) { + const errorMessage = oauthErrorDescription + ? `OAuth error: ${oauthError} - ${oauthErrorDescription}` + : `OAuth error: ${oauthError}` + logger.forBot().warn(errorMessage) + return responses.endWizard({ + success: false, + errorMessage, + }) + } + + const authorizationCode = query.get('code') + if (!authorizationCode) { + const errorMessage = + 'No authorization code received from Dropbox. ' + + 'This may happen if you denied the authorization request, ' + + 'if the authorization code expired, or if there was an error during the OAuth flow. ' + + 'Please try authorizing again.' + logger.forBot().warn(errorMessage) + return responses.endWizard({ + success: false, + errorMessage, + }) + } + + try { + const redirectUri = _getOAuthRedirectUri() + const oauthClient = new DropboxOAuthClient({ client, ctx }) + await oauthClient.processAuthorizationCode(authorizationCode, redirectUri) + logger.forBot().info('Successfully exchanged authorization code for refresh token') + + await client.configureIntegration({ identifier: ctx.webhookId }) + + return responses.endWizard({ + success: true, + }) + } catch (error) { + const errorMessage = + error instanceof Error + ? `Failed to process authorization code: ${error.message}. Please make sure the code is correct and hasn't expired.` + : 'Failed to process authorization code. Please try again.' + logger.forBot().error({ err: error }, errorMessage) + return responses.endWizard({ + success: false, + errorMessage, + }) + } +} + +const _endHandler: WizardHandler = ({ responses }) => { + return responses.endWizard({ + success: true, + }) +} diff --git a/integrations/dropbox/tsconfig.json b/integrations/dropbox/tsconfig.json index 758f7d7ec50..936f2bd558a 100644 --- a/integrations/dropbox/tsconfig.json +++ b/integrations/dropbox/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact", "baseUrl": ".", "outDir": "dist", "experimentalDecorators": true, From 9ecff3af466861ce091efa1f24b22e429c33e642 Mon Sep 17 00:00:00 2001 From: Sergei Kofman <52934469+sergeikofman444@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:02:04 -0500 Subject: [PATCH 2/3] feat(integrations/hubspot): update actions and cards (#14982) --- .../hubspot/definitions/actions/company.ts | 55 +++++++++++++++++++ integrations/hubspot/definitions/states.ts | 3 +- .../hubspot/integration.definition.ts | 2 +- integrations/hubspot/src/actions/company.ts | 43 ++++++++++++++- .../hubspot/src/hubspot-api/hubspot-client.ts | 20 +++++++ 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/integrations/hubspot/definitions/actions/company.ts b/integrations/hubspot/definitions/actions/company.ts index 5af085d3601..5f7776dfb7f 100644 --- a/integrations/hubspot/definitions/actions/company.ts +++ b/integrations/hubspot/definitions/actions/company.ts @@ -25,6 +25,61 @@ const searchCompany: ActionDefinition = { }, } +const getCompany: ActionDefinition = { + title: 'Get Company', + description: 'Get a company from Hubspot by ID', + input: { + schema: z.object({ + companyId: z.string().title('Company ID').describe('The ID of the company to get'), + propertiesToReturn: z + .array(z.string()) + .optional() + .title('Properties to Return') + .describe( + 'Additional properties to return (e.g., ["health_status", "industry"]). Default properties are always included.' + ), + }), + }, + output: { + schema: z.object({ + company: companySchema.title('Company').describe('The fetched company'), + }), + }, +} + +const updateCompany: ActionDefinition = { + title: 'Update Company', + description: 'Update a company in Hubspot', + input: { + schema: z.object({ + companyId: z.string().title('Company ID').describe('The ID of the company to update'), + properties: z + .array( + z.object({ + name: z.string().title('Property Name').describe('The name of the property (e.g., "health_status")'), + value: z.string().title('Property Value').describe('The value of the property'), + }) + ) + .title('Properties') + .describe('Properties to update on the company'), + }), + }, + output: { + schema: z.object({ + company: companySchema + .extend({ + // May not be returned by API + name: companySchema.shape.name.optional(), + domain: companySchema.shape.domain.optional(), + }) + .title('Company') + .describe('The updated company'), + }), + }, +} + export const actions = { searchCompany, + getCompany, + updateCompany, } as const diff --git a/integrations/hubspot/definitions/states.ts b/integrations/hubspot/definitions/states.ts index 0f62215abe7..72fbd42d2d1 100644 --- a/integrations/hubspot/definitions/states.ts +++ b/integrations/hubspot/definitions/states.ts @@ -82,12 +82,13 @@ const propertyCacheStateDefinition = { }), } satisfies StateDefinition -export type CrmObjectType = 'ticket' | 'deal' | 'contact' | 'lead' +export type CrmObjectType = 'ticket' | 'deal' | 'contact' | 'lead' | 'company' const propertyCacheStates = { ticketPropertyCache: propertyCacheStateDefinition, dealPropertyCache: propertyCacheStateDefinition, contactPropertyCache: propertyCacheStateDefinition, leadPropertyCache: propertyCacheStateDefinition, + companyPropertyCache: propertyCacheStateDefinition, } satisfies Record<`${CrmObjectType}PropertyCache`, StateDefinition> export const states = { diff --git a/integrations/hubspot/integration.definition.ts b/integrations/hubspot/integration.definition.ts index 3f9403f2ab1..d969750b628 100644 --- a/integrations/hubspot/integration.definition.ts +++ b/integrations/hubspot/integration.definition.ts @@ -5,7 +5,7 @@ export default new IntegrationDefinition({ name: 'hubspot', title: 'HubSpot', description: 'Manage contacts, tickets and more from your chatbot.', - version: '5.2.1', + version: '5.3.0', readme: 'hub.md', icon: 'icon.svg', configuration: { diff --git a/integrations/hubspot/src/actions/company.ts b/integrations/hubspot/src/actions/company.ts index 43f684f4ff2..733916fb472 100644 --- a/integrations/hubspot/src/actions/company.ts +++ b/integrations/hubspot/src/actions/company.ts @@ -1,6 +1,6 @@ import { z } from '@botpress/sdk' import { companySchema } from '../../definitions/actions/company' -import { getAuthenticatedHubspotClient } from '../utils' +import { getAuthenticatedHubspotClient, propertiesEntriesToRecord } from '../utils' import * as bp from '.botpress' type HubspotClient = Awaited> @@ -40,3 +40,44 @@ export const searchCompany: bp.IntegrationProps['actions']['searchCompany'] = as company: company ? _mapHsCompanyToBpCompany(company) : undefined, } } + +export const getCompany: bp.IntegrationProps['actions']['getCompany'] = async ({ ctx, client, input, logger }) => { + const hsClient = await getAuthenticatedHubspotClient({ ctx, client, logger }) + const propertyKeys = + input.propertiesToReturn && input.propertiesToReturn.length > 0 + ? input.propertiesToReturn + : await _getCompanyPropertyKeys(hsClient) + + const company = await hsClient.getCompanyById({ + companyId: Number(input.companyId), + propertiesToReturn: propertyKeys, + }) + + return { + company: _mapHsCompanyToBpCompany(company), + } +} + +export const updateCompany: bp.IntegrationProps['actions']['updateCompany'] = async ({ + ctx, + client, + input, + logger, +}) => { + const hsClient = await getAuthenticatedHubspotClient({ ctx, client, logger }) + + const additionalProperties = propertiesEntriesToRecord(input.properties ?? []) + + const updatedCompany = await hsClient.updateCompany({ + companyId: Number(input.companyId), + additionalProperties, + }) + + return { + company: { + ..._mapHsCompanyToBpCompany(updatedCompany), + name: updatedCompany.properties['name'] ?? undefined, + domain: updatedCompany.properties['domain'] ?? undefined, + }, + } +} diff --git a/integrations/hubspot/src/hubspot-api/hubspot-client.ts b/integrations/hubspot/src/hubspot-api/hubspot-client.ts index 59d0e78d596..d9ca1a4ca1b 100644 --- a/integrations/hubspot/src/hubspot-api/hubspot-client.ts +++ b/integrations/hubspot/src/hubspot-api/hubspot-client.ts @@ -89,6 +89,7 @@ export class HubspotClient { deal: PropertiesCache.create({ client, ctx, accessToken, type: 'deal' }), lead: PropertiesCache.create({ client, ctx, accessToken, type: 'lead' }), ticket: PropertiesCache.create({ client, ctx, accessToken, type: 'ticket' }), + company: PropertiesCache.create({ client, ctx, accessToken, type: 'company' }), } } @@ -223,6 +224,25 @@ export class HubspotClient { return company } + @handleErrors('Failed to update company') + public async updateCompany({ + companyId, + additionalProperties, + }: { + companyId: number + additionalProperties: Record + }) { + const resolvedProperties = await this._resolveAndCoerceProperties({ + properties: additionalProperties, + type: 'company', + }) + + const updatedCompany = await this._hsClient.crm.companies.basicApi.update(companyId.toString(), { + properties: resolvedProperties, + }) + return updatedCompany + } + @handleErrors('Failed to get ticket by ID') public async getTicketById({ ticketId, propertiesToReturn }: { ticketId: number; propertiesToReturn?: string[] }) { const ticket = await this._hsClient.crm.tickets.basicApi.getById(ticketId.toString(), [ From 513845ad4238c13fd8fd238d6db39d1ff8f508b1 Mon Sep 17 00:00:00 2001 From: Yann Allard <42552874+allardy@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:24:36 -0500 Subject: [PATCH 3/3] chore(client): bump version (#14986) --- package.json | 2 +- packages/cli/package.json | 6 +-- packages/cli/templates/empty-bot/package.json | 4 +- .../templates/empty-integration/package.json | 4 +- .../cli/templates/empty-plugin/package.json | 2 +- .../cli/templates/hello-world/package.json | 4 +- .../templates/webhook-message/package.json | 4 +- packages/client/package.json | 2 +- packages/client/src/public/index.ts | 4 -- packages/client/src/runtime/index.ts | 4 -- packages/cognitive/package.json | 2 +- packages/llmz/package.json | 4 +- packages/sdk/package.json | 4 +- packages/vai/package.json | 2 +- packages/zai/package.json | 4 +- plugins/conversation-insights/package.json | 2 +- pnpm-lock.yaml | 44 +++++++++---------- 17 files changed, 45 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index d228e6b1a8f..784e0cbdd84 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "@aws-sdk/client-dynamodb": "^3.564.0", - "@botpress/api": "1.73.0", + "@botpress/api": "1.73.4", "@botpress/cli": "workspace:*", "@botpress/client": "workspace:*", "@botpress/sdk": "workspace:*", diff --git a/packages/cli/package.json b/packages/cli/package.json index b95e6a4cb07..860033a2606 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "5.5.6", + "version": "5.5.7", "description": "Botpress CLI", "scripts": { "build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen", @@ -27,8 +27,8 @@ "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.7.0", "@botpress/chat": "0.5.5", - "@botpress/client": "1.35.0", - "@botpress/sdk": "5.4.3", + "@botpress/client": "1.36.0", + "@botpress/sdk": "5.4.4", "@bpinternal/const": "^0.1.0", "@bpinternal/tunnel": "^0.1.1", "@bpinternal/verel": "^0.2.0", diff --git a/packages/cli/templates/empty-bot/package.json b/packages/cli/templates/empty-bot/package.json index 9ef6f270551..9ffb01eb194 100644 --- a/packages/cli/templates/empty-bot/package.json +++ b/packages/cli/templates/empty-bot/package.json @@ -5,8 +5,8 @@ }, "private": true, "dependencies": { - "@botpress/client": "1.35.0", - "@botpress/sdk": "5.4.3" + "@botpress/client": "1.36.0", + "@botpress/sdk": "5.4.4" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-integration/package.json b/packages/cli/templates/empty-integration/package.json index 038a00ef412..bf56707cc05 100644 --- a/packages/cli/templates/empty-integration/package.json +++ b/packages/cli/templates/empty-integration/package.json @@ -6,8 +6,8 @@ }, "private": true, "dependencies": { - "@botpress/client": "1.35.0", - "@botpress/sdk": "5.4.3" + "@botpress/client": "1.36.0", + "@botpress/sdk": "5.4.4" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-plugin/package.json b/packages/cli/templates/empty-plugin/package.json index 9a5de32cae4..a225d35647b 100644 --- a/packages/cli/templates/empty-plugin/package.json +++ b/packages/cli/templates/empty-plugin/package.json @@ -6,7 +6,7 @@ }, "private": true, "dependencies": { - "@botpress/sdk": "5.4.3" + "@botpress/sdk": "5.4.4" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/hello-world/package.json b/packages/cli/templates/hello-world/package.json index 62cfb6dc218..4065fec4807 100644 --- a/packages/cli/templates/hello-world/package.json +++ b/packages/cli/templates/hello-world/package.json @@ -6,8 +6,8 @@ }, "private": true, "dependencies": { - "@botpress/client": "1.35.0", - "@botpress/sdk": "5.4.3" + "@botpress/client": "1.36.0", + "@botpress/sdk": "5.4.4" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/webhook-message/package.json b/packages/cli/templates/webhook-message/package.json index fd0c49a1593..95aefee706c 100644 --- a/packages/cli/templates/webhook-message/package.json +++ b/packages/cli/templates/webhook-message/package.json @@ -6,8 +6,8 @@ }, "private": true, "dependencies": { - "@botpress/client": "1.35.0", - "@botpress/sdk": "5.4.3", + "@botpress/client": "1.36.0", + "@botpress/sdk": "5.4.4", "axios": "^1.6.8" }, "devDependencies": { diff --git a/packages/client/package.json b/packages/client/package.json index f1ea40d7125..f804c283901 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/client", - "version": "1.35.0", + "version": "1.36.0", "description": "Botpress Client", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/client/src/public/index.ts b/packages/client/src/public/index.ts index 82c407980c0..a8a71e56235 100644 --- a/packages/client/src/public/index.ts +++ b/packages/client/src/public/index.ts @@ -62,10 +62,6 @@ export class Client extends gen.Client implements IClient { new common.listing.AsyncCollection(({ nextToken }) => this.listUsers({ nextToken, ...props }).then((r) => ({ ...r, items: r.users })) ), - tasks: (props: ListInputs['listTasks']) => - new common.listing.AsyncCollection(({ nextToken }) => - this.listTasks({ nextToken, ...props }).then((r) => ({ ...r, items: r.tasks })) - ), publicIntegrations: (props: ListInputs['listPublicIntegrations']) => new common.listing.AsyncCollection(({ nextToken }) => this.listPublicIntegrations({ nextToken, ...props }).then((r) => ({ ...r, items: r.integrations })) diff --git a/packages/client/src/runtime/index.ts b/packages/client/src/runtime/index.ts index 796d5b0f34b..3ac280e853a 100644 --- a/packages/client/src/runtime/index.ts +++ b/packages/client/src/runtime/index.ts @@ -56,10 +56,6 @@ export class Client extends gen.Client { new common.listing.AsyncCollection(({ nextToken }) => this.listUsers({ nextToken, ...props }).then((r) => ({ ...r, items: r.users })) ), - tasks: (props: ListInputs['listTasks']) => - new common.listing.AsyncCollection(({ nextToken }) => - this.listTasks({ nextToken, ...props }).then((r) => ({ ...r, items: r.tasks })) - ), } } } diff --git a/packages/cognitive/package.json b/packages/cognitive/package.json index ffb82777c20..d9123ee3476 100644 --- a/packages/cognitive/package.json +++ b/packages/cognitive/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cognitive", - "version": "0.3.14", + "version": "0.3.15", "description": "Wrapper around the Botpress Client to call LLMs", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/llmz/package.json b/packages/llmz/package.json index bc3bfc84e9b..37f72814adc 100644 --- a/packages/llmz/package.json +++ b/packages/llmz/package.json @@ -71,8 +71,8 @@ "tsx": "^4.19.2" }, "peerDependencies": { - "@botpress/client": "1.35.0", - "@botpress/cognitive": "0.3.14", + "@botpress/client": "1.36.0", + "@botpress/cognitive": "0.3.15", "@bpinternal/thicktoken": "^2.0.0", "@bpinternal/zui": "^1.3.3" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 0dd6724847e..5f2ad7a780b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/sdk", - "version": "5.4.3", + "version": "5.4.4", "description": "Botpress SDK", "main": "./dist/index.cjs", "module": "./dist/index.mjs", @@ -20,7 +20,7 @@ "author": "", "license": "MIT", "dependencies": { - "@botpress/client": "1.35.0", + "@botpress/client": "1.36.0", "browser-or-node": "^2.1.1", "semver": "^7.3.8" }, diff --git a/packages/vai/package.json b/packages/vai/package.json index b7e8a51609e..e85c2ec7ebf 100644 --- a/packages/vai/package.json +++ b/packages/vai/package.json @@ -40,7 +40,7 @@ "tsup": "^8.0.2" }, "peerDependencies": { - "@botpress/client": "1.35.0", + "@botpress/client": "1.36.0", "@bpinternal/thicktoken": "^1.0.1", "@bpinternal/zui": "^1.3.3", "lodash": "^4.17.21", diff --git a/packages/zai/package.json b/packages/zai/package.json index 8b4b141b001..c880763c0a6 100644 --- a/packages/zai/package.json +++ b/packages/zai/package.json @@ -1,7 +1,7 @@ { "name": "@botpress/zai", "description": "Zui AI (zai) – An LLM utility library written on top of Zui and the Botpress API", - "version": "2.6.0", + "version": "2.6.1", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -32,7 +32,7 @@ "author": "", "license": "ISC", "dependencies": { - "@botpress/cognitive": "0.3.14", + "@botpress/cognitive": "0.3.15", "json5": "^2.2.3", "jsonrepair": "^3.10.0", "lodash-es": "^4.17.21", diff --git a/plugins/conversation-insights/package.json b/plugins/conversation-insights/package.json index 2578ca64899..4267f9546d6 100644 --- a/plugins/conversation-insights/package.json +++ b/plugins/conversation-insights/package.json @@ -7,7 +7,7 @@ }, "private": true, "dependencies": { - "@botpress/cognitive": "0.3.14", + "@botpress/cognitive": "0.3.15", "@botpress/sdk": "workspace:*", "browser-or-node": "^2.1.1", "jsonrepair": "^3.10.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad239cf09b6..774b3c24ed6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,8 +17,8 @@ importers: specifier: ^3.564.0 version: 3.709.0 '@botpress/api': - specifier: 1.73.0 - version: 1.73.0 + specifier: 1.73.4 + version: 1.73.4 '@botpress/cli': specifier: workspace:* version: link:packages/cli @@ -2502,10 +2502,10 @@ importers: specifier: 0.5.5 version: link:../chat-client '@botpress/client': - specifier: 1.35.0 + specifier: 1.36.0 version: link:../client '@botpress/sdk': - specifier: 5.4.3 + specifier: 5.4.4 version: link:../sdk '@bpinternal/const': specifier: ^0.1.0 @@ -2626,10 +2626,10 @@ importers: packages/cli/templates/empty-bot: dependencies: '@botpress/client': - specifier: 1.35.0 + specifier: 1.36.0 version: link:../../../client '@botpress/sdk': - specifier: 5.4.3 + specifier: 5.4.4 version: link:../../../sdk devDependencies: '@types/node': @@ -2642,10 +2642,10 @@ importers: packages/cli/templates/empty-integration: dependencies: '@botpress/client': - specifier: 1.35.0 + specifier: 1.36.0 version: link:../../../client '@botpress/sdk': - specifier: 5.4.3 + specifier: 5.4.4 version: link:../../../sdk devDependencies: '@types/node': @@ -2658,7 +2658,7 @@ importers: packages/cli/templates/empty-plugin: dependencies: '@botpress/sdk': - specifier: 5.4.3 + specifier: 5.4.4 version: link:../../../sdk devDependencies: '@types/node': @@ -2671,10 +2671,10 @@ importers: packages/cli/templates/hello-world: dependencies: '@botpress/client': - specifier: 1.35.0 + specifier: 1.36.0 version: link:../../../client '@botpress/sdk': - specifier: 5.4.3 + specifier: 5.4.4 version: link:../../../sdk devDependencies: '@types/node': @@ -2687,10 +2687,10 @@ importers: packages/cli/templates/webhook-message: dependencies: '@botpress/client': - specifier: 1.35.0 + specifier: 1.36.0 version: link:../../../client '@botpress/sdk': - specifier: 5.4.3 + specifier: 5.4.4 version: link:../../../sdk axios: specifier: ^1.6.8 @@ -2841,10 +2841,10 @@ importers: specifier: ^7.26.3 version: 7.26.9 '@botpress/client': - specifier: 1.35.0 + specifier: 1.36.0 version: link:../client '@botpress/cognitive': - specifier: 0.3.14 + specifier: 0.3.15 version: link:../cognitive '@bpinternal/thicktoken': specifier: ^2.0.0 @@ -2947,7 +2947,7 @@ importers: packages/sdk: dependencies: '@botpress/client': - specifier: 1.35.0 + specifier: 1.36.0 version: link:../client '@bpinternal/zui': specifier: ^1.3.3 @@ -2984,7 +2984,7 @@ importers: packages/vai: dependencies: '@botpress/client': - specifier: 1.35.0 + specifier: 1.36.0 version: link:../client '@bpinternal/thicktoken': specifier: ^1.0.1 @@ -3030,7 +3030,7 @@ importers: packages/zai: dependencies: '@botpress/cognitive': - specifier: 0.3.14 + specifier: 0.3.15 version: link:../cognitive '@bpinternal/thicktoken': specifier: ^1.0.0 @@ -3149,7 +3149,7 @@ importers: plugins/conversation-insights: dependencies: '@botpress/cognitive': - specifier: 0.3.14 + specifier: 0.3.15 version: link:../../packages/cognitive '@botpress/sdk': specifier: workspace:* @@ -3991,8 +3991,8 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@botpress/api@1.73.0': - resolution: {integrity: sha512-7Z3WGx8hVhc5JcN9DwdWA6lLsA4b/qcBoEHM0RWuqVQacEcFbdFgedaR4FDh7qdGxVQxEhHpwPjJv0CNbndpGA==} + '@botpress/api@1.73.4': + resolution: {integrity: sha512-pMXnEVyNfqwPBibccS/8t8s6Orzq/FSYEdYe9ukRrJLl7hWo60hClpvb0cAldh6RnAO+y/3l6JCQyHk0F9wjpg==} '@bpinternal/const@0.1.0': resolution: {integrity: sha512-iIQg9oYYXOt+LSK34oNhJVQTcgRdtLmLZirEUaE+R9hnmbKONA5reR2kTewxZmekGyxej+5RtDK9xrC/0hmeAw==} @@ -13167,7 +13167,7 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@botpress/api@1.73.0': + '@botpress/api@1.73.4': dependencies: '@bpinternal/opapi': 1.0.0(openapi-types@12.1.3) transitivePeerDependencies: