import * as oauthWizard from '@botpress/common/src/oauth-wizard' import { OAUTH_IDENTIFIER_HEADER } from '@botpress/sdk' import { Signature } from '@hubspot/api-client' import { getClientSecret } from '../auth' import { handleOperatorReplied } from '../hitl/events/operator-replied' import { validateHubSpotSignature } from '../hitl/utils/signature' import * as handlers from './handlers' import { buildOAuthWizard } from './handlers/oauth-wizard' import * as bp from '.botpress' export const handler: bp.IntegrationProps['handler'] = async (props) => { const { req, logger } = props logger.debug(`Received request on ${req.path}: ${JSON.stringify(req.body, null, 2)}`) if (oauthWizard.isOAuthWizardUrl(req.path)) { try { return await buildOAuthWizard(props).handleRequest() } catch (thrown: unknown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) return oauthWizard.generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) } } if (req.path.startsWith('/oauth')) { const modifiedProps = { ...props, req: { ...props.req, path: '/oauth/wizard/oauth-callback' } } try { const wizardResult = await buildOAuthWizard(modifiedProps).handleRequest() const identifier = wizardResult.headers?.[OAUTH_IDENTIFIER_HEADER] return identifier ? { status: 200, headers: { [OAUTH_IDENTIFIER_HEADER]: identifier }, } : wizardResult } catch (thrown: unknown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) return oauthWizard.generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) } } if (_isCustomChannelEvent(req.body)) { return await _handleHitlEvent(props) } // Global webhook subscriptions (conversation updates + CRM events) if (handlers.isConversationEvent(props) || handlers.isBatchUpdateEvent(props)) { const validation = await _validateRequestAuthentication(props) if (validation.error) { logger.error(`Error validating request: ${validation.message}`) return { status: 401, body: validation.message } } if (handlers.isConversationEvent(props)) { return await handlers.handleConversationEvent(props) } return await handlers.handleBatchUpdateEvent(props) } logger.warn('No handler found for request') } const _isCustomChannelEvent = (body: string | undefined): boolean => { if (!body?.length) return false try { const parsed = typeof body === 'string' ? JSON.parse(body) : body return typeof parsed === 'object' && !Array.isArray(parsed) && typeof parsed.type === 'string' } catch { return false } } const _handleHitlEvent: bp.IntegrationProps['handler'] = async ({ req, ctx, client, logger }) => { const signature = req.headers['x-hubspot-signature-v3'] as string const timestamp = req.headers['x-hubspot-request-timestamp'] as string const rawBody = typeof req.body === 'string' ? req.body : JSON.stringify(req.body) const webhookUrl = `${process.env.BP_WEBHOOK_URL}/${ctx.webhookId}` const clientSecret = await getClientSecret({ client, ctx }) if (clientSecret) { const isValid = validateHubSpotSignature( rawBody, signature, timestamp, req.method, webhookUrl, clientSecret, logger ) if (!isValid) { logger.forBot().error('Invalid HubSpot v3 signature — rejecting HITL event') return { status: 401, body: 'Invalid signature' } } logger.forBot().info('HubSpot v3 webhook signature verified') } else { logger.forBot().warn('No client secret configured — skipping HITL webhook signature validation') } let payload: any try { payload = typeof req.body === 'string' ? JSON.parse(req.body) : req.body } catch (err) { logger.forBot().error('Failed to parse HITL request body:', err) return { status: 400, body: 'Invalid JSON body' } } if (payload.type === 'OUTGOING_CHANNEL_MESSAGE_CREATED') { return await handleOperatorReplied({ hubspotEvent: payload, client, logger }) } if (payload.type === 'CHANNEL_ACCOUNT_CREATED') { logger.forBot().info(`Channel account created: ${JSON.stringify(payload)}`) return {} } logger.forBot().warn(`Unhandled HubSpot HITL event format: ${payload.type}`) return {} } const _validateRequestAuthentication = async ({ req, ctx, client }: bp.HandlerProps) => { const clientSecret = await getClientSecret({ client, ctx }) if (!clientSecret) { return { error: false } } const signature = req.headers['x-hubspot-signature'] if (!signature) { return { error: true, message: 'Missing "x-hubspot-signature" header' } } const isValid = Signature.isValid({ clientSecret, requestBody: req.body ?? '', signature, }) if (!isValid) { return { error: true, message: 'Invalid signature' } } return { error: false } }