diff --git a/integrations/airtable/integration.definition.ts b/integrations/airtable/integration.definition.ts index 5da94128a49..d2127f3b54b 100644 --- a/integrations/airtable/integration.definition.ts +++ b/integrations/airtable/integration.definition.ts @@ -19,7 +19,7 @@ export default new IntegrationDefinition({ title: 'Airtable', description: 'Access and manage Airtable data to allow your chatbot to retrieve details, update records, and organize information.', - version: '3.0.0', + version: '3.0.2', readme: 'hub.md', icon: 'icon.svg', configuration: { diff --git a/integrations/airtable/src/oauth-wizard/index.ts b/integrations/airtable/src/oauth-wizard/index.ts index 95a2a8bd3ac..424b8163d2d 100644 --- a/integrations/airtable/src/oauth-wizard/index.ts +++ b/integrations/airtable/src/oauth-wizard/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/integrations/bamboohr/integration.definition.ts b/integrations/bamboohr/integration.definition.ts index 08ff5c62c53..c1da985fd77 100644 --- a/integrations/bamboohr/integration.definition.ts +++ b/integrations/bamboohr/integration.definition.ts @@ -3,7 +3,7 @@ import { IntegrationDefinition, z } from '@botpress/sdk' import { actions, events, subdomain } from './definitions' export const INTEGRATION_NAME = 'bamboohr' -export const INTEGRATION_VERSION = '2.1.2' +export const INTEGRATION_VERSION = '2.1.3' export default new IntegrationDefinition({ name: INTEGRATION_NAME, diff --git a/integrations/bamboohr/src/handlers/oauth.ts b/integrations/bamboohr/src/handlers/oauth.ts index 42c0aac6f2e..196585b75be 100644 --- a/integrations/bamboohr/src/handlers/oauth.ts +++ b/integrations/bamboohr/src/handlers/oauth.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from '../wizard' import * as bp from '.botpress' diff --git a/integrations/calendly/integration.definition.ts b/integrations/calendly/integration.definition.ts index 286911f3805..df4051e25fd 100644 --- a/integrations/calendly/integration.definition.ts +++ b/integrations/calendly/integration.definition.ts @@ -5,7 +5,7 @@ import { inviteeEventOutputSchema } from 'definitions/events' export default new IntegrationDefinition({ name: 'calendly', title: 'Calendly', - version: '0.0.5', + version: '0.0.6', readme: 'hub.md', icon: 'icon.svg', description: 'Schedule meetings and manage events using the Calendly scheduling platform.', diff --git a/integrations/calendly/package.json b/integrations/calendly/package.json index f58ac7bcd59..b0cf8d7e57c 100644 --- a/integrations/calendly/package.json +++ b/integrations/calendly/package.json @@ -9,6 +9,7 @@ "private": true, "dependencies": { "@botpress/client": "workspace:*", + "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "axios": "^1.11.0" }, diff --git a/integrations/calendly/src/calendly-api/auth.ts b/integrations/calendly/src/calendly-api/auth.ts index e9a5ad3b431..bfe9fcabf39 100644 --- a/integrations/calendly/src/calendly-api/auth.ts +++ b/integrations/calendly/src/calendly-api/auth.ts @@ -1,3 +1,4 @@ +import { handleErrorsDecorator as handleErrors } from '@botpress/common' import { RuntimeError } from '@botpress/sdk' import axios, { type AxiosInstance } from 'axios' import type { CommonHandlerProps, Result } from '../types' @@ -47,6 +48,7 @@ export class CalendlyAuthClient { return { success: true, data: result.data } } + @handleErrors('Failed to obtain Calendly OAuth access token from authorization code') public async getAccessTokenWithCode(code: string): Promise> { return this._getAccessToken({ grant_type: 'authorization_code', @@ -55,6 +57,7 @@ export class CalendlyAuthClient { }) } + @handleErrors('Failed to refresh Calendly OAuth access token') public async getAccessTokenWithRefreshToken(refreshToken: string): Promise> { return this._getAccessToken({ grant_type: 'refresh_token', diff --git a/integrations/calendly/src/handler.ts b/integrations/calendly/src/handler.ts index 7b516b25662..3576fe6b56a 100644 --- a/integrations/calendly/src/handler.ts +++ b/integrations/calendly/src/handler.ts @@ -1,3 +1,4 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import { exchangeAuthCodeForRefreshToken } from './calendly-api/auth' import { dispatchIntegrationEvent } from './webhooks/event-dispatcher' import { parseWebhookEvent, verifyWebhookSignature } from './webhooks/webhook-utils' @@ -7,24 +8,40 @@ const _isOauthRequest = ({ req }: bp.HandlerProps) => req.path === '/oauth' export const handler = async (props: bp.HandlerProps) => { if (_isOauthRequest(props)) { - const oAuthCode = new URLSearchParams(props.req.query).get('code') - if (oAuthCode === null) throw new Error('Missing authentication code') + try { + const searchParams = new URLSearchParams(props.req.query) + const error = searchParams.get('error') + if (error) { + throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`) + } - await exchangeAuthCodeForRefreshToken(props, oAuthCode) - return + const oAuthCode = searchParams.get('code') + if (!oAuthCode) { + throw new Error('Authorization code not present in OAuth callback') + } + + await exchangeAuthCodeForRefreshToken(props, oAuthCode) + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + props.logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) + } } const signatureResult = await verifyWebhookSignature(props) if (!signatureResult.success) { props.logger.forBot().error(signatureResult.error.message, signatureResult.error) - return + return {} } const result = parseWebhookEvent(props) if (!result.success) { props.logger.forBot().error(result.error.message, result.error) - return + return {} } await dispatchIntegrationEvent(props, result.data) + return {} } diff --git a/integrations/calendly/tsconfig.json b/integrations/calendly/tsconfig.json index d46abc5b88f..32b8ead2675 100644 --- a/integrations/calendly/tsconfig.json +++ b/integrations/calendly/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "paths": { "*": ["./*"] }, - "outDir": "dist" + "outDir": "dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"] } diff --git a/integrations/docusign/integration.definition.ts b/integrations/docusign/integration.definition.ts index 8abee528a4c..8cf68a91d43 100644 --- a/integrations/docusign/integration.definition.ts +++ b/integrations/docusign/integration.definition.ts @@ -6,7 +6,7 @@ import { envelopeEventSchema } from 'definitions/events' export default new IntegrationDefinition({ name: 'docusign', title: 'Docusign', - version: '2.1.2', + version: '2.1.3', readme: 'hub.md', icon: 'icon.svg', description: diff --git a/integrations/docusign/package.json b/integrations/docusign/package.json index 7984cc68bac..07875bc369e 100644 --- a/integrations/docusign/package.json +++ b/integrations/docusign/package.json @@ -9,6 +9,7 @@ "private": true, "dependencies": { "@botpress/client": "workspace:*", + "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "axios": "^1.12.2", "docusign-esign": "^8.4.0" diff --git a/integrations/docusign/src/docusign-api/auth.ts b/integrations/docusign/src/docusign-api/auth.ts index d7fd553e015..bbb57220af1 100644 --- a/integrations/docusign/src/docusign-api/auth.ts +++ b/integrations/docusign/src/docusign-api/auth.ts @@ -1,3 +1,4 @@ +import { handleErrorsDecorator as handleErrors } from '@botpress/common' import { RuntimeError } from '@botpress/sdk' import axios, { type AxiosInstance } from 'axios' import type { CommonHandlerProps, Result } from '../types' @@ -74,6 +75,7 @@ export class DocusignAuthClient { } } + @handleErrors('Failed to obtain Docusign OAuth access token from authorization code') public async getAccessTokenWithCode(code: string): Promise> { return this._getAccessToken({ grant_type: 'authorization_code', @@ -81,6 +83,7 @@ export class DocusignAuthClient { }) } + @handleErrors('Failed to refresh Docusign OAuth access token') public async getAccessTokenWithRefreshToken(refreshToken: string): Promise> { return this._getAccessToken({ grant_type: 'refresh_token', @@ -88,6 +91,7 @@ export class DocusignAuthClient { }) } + @handleErrors('Failed to retrieve Docusign user info') public async getUserInfo(accessToken: string, tokenType: string): Promise> { const resp = await this._axiosClient.get('/oauth/userinfo', { headers: { diff --git a/integrations/docusign/src/handler.ts b/integrations/docusign/src/handler.ts index 7551d44e525..492f30b09a6 100644 --- a/integrations/docusign/src/handler.ts +++ b/integrations/docusign/src/handler.ts @@ -1,3 +1,4 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import { exchangeAuthCodeForRefreshToken } from './docusign-api/auth-utils' import { dispatchIntegrationEvent } from './webhooks/event-dispatcher' import { parseWebhookEvent, verifyWebhookSignature } from './webhooks/utils' @@ -7,24 +8,40 @@ const _isOauthRequest = ({ req }: bp.HandlerProps) => req.path === '/oauth' export const handler = async (props: bp.HandlerProps) => { if (_isOauthRequest(props)) { - const oAuthCode = new URLSearchParams(props.req.query).get('code') - if (oAuthCode === null) throw new Error('Missing authentication code') + try { + const searchParams = new URLSearchParams(props.req.query) + const error = searchParams.get('error') + if (error) { + throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`) + } - await exchangeAuthCodeForRefreshToken(props, oAuthCode) - return + const oAuthCode = searchParams.get('code') + if (!oAuthCode) { + throw new Error('Authorization code not present in OAuth callback') + } + + await exchangeAuthCodeForRefreshToken(props, oAuthCode) + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + props.logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) + } } const signatureResult = verifyWebhookSignature(props) if (!signatureResult.success) { props.logger.forBot().error(signatureResult.error.message, signatureResult.error) - return + return {} } const result = parseWebhookEvent(props) if (!result.success) { props.logger.forBot().error(result.error.message, result.error) - return + return {} } await dispatchIntegrationEvent(props, result.data) + return {} } diff --git a/integrations/docusign/tsconfig.json b/integrations/docusign/tsconfig.json index d46abc5b88f..32b8ead2675 100644 --- a/integrations/docusign/tsconfig.json +++ b/integrations/docusign/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "paths": { "*": ["./*"] }, - "outDir": "dist" + "outDir": "dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"] } diff --git a/integrations/dropbox/integration.definition.ts b/integrations/dropbox/integration.definition.ts index 3e273f05ec8..ddfef6aeb05 100644 --- a/integrations/dropbox/integration.definition.ts +++ b/integrations/dropbox/integration.definition.ts @@ -5,7 +5,7 @@ import { actions, configuration, configurations, entities, secrets, states } fro export default new sdk.IntegrationDefinition({ name: 'dropbox', title: 'Dropbox', - version: '2.0.2', + version: '2.0.3', description: 'Manage your files and folders effortlessly.', readme: 'hub.md', icon: 'icon.svg', diff --git a/integrations/dropbox/src/webhook-events/oauth/index.ts b/integrations/dropbox/src/webhook-events/oauth/index.ts index 58909ec171e..27237b6829b 100644 --- a/integrations/dropbox/src/webhook-events/oauth/index.ts +++ b/integrations/dropbox/src/webhook-events/oauth/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/integrations/github/integration.definition.ts b/integrations/github/integration.definition.ts index 0fc384c7eb3..d08d2ad863d 100644 --- a/integrations/github/integration.definition.ts +++ b/integrations/github/integration.definition.ts @@ -8,7 +8,7 @@ import { actions, events, configuration, configurations, channels, user, secrets export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, title: 'GitHub', - version: '1.2.1', + version: '1.2.2', icon: 'icon.svg', readme: 'hub.md', description: 'Manage GitHub issues, pull requests, and repositories.', diff --git a/integrations/github/src/handler.ts b/integrations/github/src/handler.ts index a061030e618..1818604bd34 100644 --- a/integrations/github/src/handler.ts +++ b/integrations/github/src/handler.ts @@ -1,3 +1,4 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import * as sdk from '@botpress/sdk' import { verify as verifyWebhook } from '@octokit/webhooks-methods' import type { WebhookEvent } from '@octokit/webhooks-types' @@ -69,10 +70,15 @@ const _isOauthRequest = ({ req }: bp.HandlerProps) => req.path === '/oauth' const _handleOauthRequest = async ({ req, client, ctx, logger }: bp.HandlerProps) => { logger.forBot().info('Handling incoming OAuth callback') - return _handleOauth(req, client, ctx).catch((err) => { - logger.forBot().error('Error while processing OAuth callback', err.response?.data || err.message) - throw err - }) + try { + await _handleOauth(req, client, ctx) + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) + } } const _handleOauth = async (req: sdk.Request, client: bp.Client, ctx: bp.Context) => { diff --git a/integrations/github/src/misc/error-handling.ts b/integrations/github/src/misc/error-handling.ts index 70ff4195035..097db759dfd 100644 --- a/integrations/github/src/misc/error-handling.ts +++ b/integrations/github/src/misc/error-handling.ts @@ -1,7 +1 @@ -import { createAsyncFnWrapperWithErrorRedaction } from '@botpress/common' -import * as sdk from '@botpress/sdk' - -export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction((originalError, customErrorMessage) => { - console.info(customErrorMessage, originalError) - return new sdk.RuntimeError(customErrorMessage) -}) +export { wrapAsyncFnWithTryCatch, handleErrorsDecorator } from '@botpress/common' diff --git a/integrations/gmail/integration.definition.ts b/integrations/gmail/integration.definition.ts index 63b0bb5dd15..d620d5357b5 100644 --- a/integrations/gmail/integration.definition.ts +++ b/integrations/gmail/integration.definition.ts @@ -12,7 +12,7 @@ import { } from './definitions' export const INTEGRATION_NAME = 'gmail' -export const INTEGRATION_VERSION = '1.0.7' +export const INTEGRATION_VERSION = '1.0.8' export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, diff --git a/integrations/gmail/src/google-api/google-client.ts b/integrations/gmail/src/google-api/google-client.ts index cce11fdfab0..d3b9c38ab01 100644 --- a/integrations/gmail/src/google-api/google-client.ts +++ b/integrations/gmail/src/google-api/google-client.ts @@ -2,6 +2,7 @@ import * as sdk from '@botpress/sdk' import { gmail_v1, google } from 'googleapis' import { IntegrationConfig } from 'src/config/integration-config' import { composeRawEmail } from 'src/utils/mail-composing' +import { handleErrorsDecorator as handleErrors } from './error-handling' import { GmailClient, GoogleOAuth2Client } from './types' import * as bp from '.botpress' @@ -53,6 +54,7 @@ export class GoogleClient { return new GoogleClient(gmailClient, topicName) } + @handleErrors('Failed to obtain Gmail OAuth refresh token from authorization code') public static async createFromAuthorizationCode({ client, ctx, diff --git a/integrations/gmail/src/webhook-events/handler.ts b/integrations/gmail/src/webhook-events/handler.ts index 5d6f5e6626d..27d2a3ab041 100644 --- a/integrations/gmail/src/webhook-events/handler.ts +++ b/integrations/gmail/src/webhook-events/handler.ts @@ -20,6 +20,7 @@ export const handler = async (props: bp.HandlerProps) => { } await handleIncomingEmail(props) + return {} } /* diff --git a/integrations/gmail/src/webhook-events/oauth-callback.ts b/integrations/gmail/src/webhook-events/oauth-callback.ts index af1efce870a..add6c15f7fa 100644 --- a/integrations/gmail/src/webhook-events/oauth-callback.ts +++ b/integrations/gmail/src/webhook-events/oauth-callback.ts @@ -1,44 +1,48 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import { GoogleClient } from '../google-api' import * as bp from '.botpress' export const handleOAuthCallback = async ({ req, client, ctx, logger }: bp.HandlerProps) => { logger.forBot().info('Starting OAuth callback handling') - const searchParams = new URLSearchParams(req.query) - const authorizationCode = searchParams.get('code') - - if (!authorizationCode) { - logger.forBot().error('Error extracting code from url') - return - } - - logger.forBot().info('Creating Google client from authorization code') - let googleClient: GoogleClient try { - googleClient = await GoogleClient.createFromAuthorizationCode({ + const searchParams = new URLSearchParams(req.query) + const error = searchParams.get('error') + if (error) { + throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`) + } + + const authorizationCode = searchParams.get('code') + if (!authorizationCode) { + throw new Error('Authorization code not present in OAuth callback') + } + + logger.forBot().info('Creating Google client from authorization code') + const googleClient = await GoogleClient.createFromAuthorizationCode({ client, ctx, authorizationCode, }) logger.forBot().info('Google client created successfully') - } catch (error) { - logger.forBot().error(`Failed to create Google client: ${error instanceof Error ? error.message : String(error)}`) - throw error - } - logger.forBot().info('Retrieving user email from Google profile') - const userEmail = await googleClient.getMyEmail() + logger.forBot().info('Retrieving user email from Google profile') + const userEmail = await googleClient.getMyEmail() + if (!userEmail) { + throw new Error('Failed to extract email from Google profile') + } - if (!userEmail) { - logger.forBot().error('Error extracting email from profile') - return - } - - logger.forBot().info(`User email retrieved: ${userEmail}`) + logger.forBot().info(`User email retrieved: ${userEmail}`) - logger.forBot().info(`Configuring integration for user: ${userEmail}`) - await client.configureIntegration({ - identifier: userEmail, - }) - logger.forBot().info('Integration configured successfully') + logger.forBot().info(`Configuring integration for user: ${userEmail}`) + await client.configureIntegration({ + identifier: userEmail, + }) + logger.forBot().info('Integration configured successfully') + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) + } } diff --git a/integrations/googlecalendar/integration.definition.ts b/integrations/googlecalendar/integration.definition.ts index 7a10c865325..b7be1390f5e 100644 --- a/integrations/googlecalendar/integration.definition.ts +++ b/integrations/googlecalendar/integration.definition.ts @@ -2,7 +2,7 @@ import * as sdk from '@botpress/sdk' import { actions, entities, configuration, configurations, identifier, events, secrets, states } from './definitions' export const INTEGRATION_NAME = 'googlecalendar' -export const INTEGRATION_VERSION = '2.0.9' +export const INTEGRATION_VERSION = '2.0.10' export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, diff --git a/integrations/googlecalendar/src/google-api/google-client.ts b/integrations/googlecalendar/src/google-api/google-client.ts index e6a09d5e05d..061a8b2ecc9 100644 --- a/integrations/googlecalendar/src/google-api/google-client.ts +++ b/integrations/googlecalendar/src/google-api/google-client.ts @@ -24,6 +24,7 @@ export class GoogleClient { }) } + @handleErrors('Failed to authenticate Google Calendar with authorization code') public static async authenticateWithAuthorizationCode({ ctx, client, diff --git a/integrations/googlecalendar/src/webhook-events/handlers/oauth-callback.ts b/integrations/googlecalendar/src/webhook-events/handlers/oauth-callback.ts index 4a8cad216d2..3814c04e600 100644 --- a/integrations/googlecalendar/src/webhook-events/handlers/oauth-callback.ts +++ b/integrations/googlecalendar/src/webhook-events/handlers/oauth-callback.ts @@ -1,21 +1,34 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import { GoogleClient } from 'src/google-api' import * as bp from '.botpress' export const oauthCallbackHandler = async ({ client, ctx, req, logger }: bp.HandlerProps) => { - const searchParams = new URLSearchParams(req.query) - const authorizationCode = searchParams.get('code') + try { + const searchParams = new URLSearchParams(req.query) + const error = searchParams.get('error') + if (error) { + throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`) + } - if (!authorizationCode) { - return - } + const authorizationCode = searchParams.get('code') + if (!authorizationCode) { + throw new Error('Authorization code not present in OAuth callback') + } - await GoogleClient.authenticateWithAuthorizationCode({ - client, - ctx, - authorizationCode, - }) + await GoogleClient.authenticateWithAuthorizationCode({ + client, + ctx, + authorizationCode, + }) - await client.configureIntegration({ identifier: ctx.webhookId }) + await client.configureIntegration({ identifier: ctx.webhookId }) - logger.forBot().info('Successfully authenticated with Google Calendar') + logger.forBot().info('Successfully authenticated with Google Calendar') + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) + } } diff --git a/integrations/hubspot/integration.definition.ts b/integrations/hubspot/integration.definition.ts index 46eb2b34503..cd065934486 100644 --- a/integrations/hubspot/integration.definition.ts +++ b/integrations/hubspot/integration.definition.ts @@ -6,7 +6,7 @@ export default new IntegrationDefinition({ name: 'hubspot', title: 'HubSpot', description: 'Manage contacts, tickets and more from your chatbot.', - version: '6.0.8', + version: '6.0.9', readme: 'hub.md', icon: 'icon.svg', configuration: { diff --git a/integrations/hubspot/src/webhook/handler.ts b/integrations/hubspot/src/webhook/handler.ts index 19eac5c5b19..3e76faffbd4 100644 --- a/integrations/hubspot/src/webhook/handler.ts +++ b/integrations/hubspot/src/webhook/handler.ts @@ -1,4 +1,3 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' import * as oauthWizard from '@botpress/common/src/oauth-wizard' import { OAUTH_IDENTIFIER_HEADER } from '@botpress/sdk' import { Signature } from '@hubspot/api-client' @@ -19,7 +18,7 @@ export const handler: bp.IntegrationProps['handler'] = async (props) => { return await buildOAuthWizard(props).handleRequest() } catch (thrown: unknown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - return generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) + return oauthWizard.generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) } } @@ -36,7 +35,7 @@ export const handler: bp.IntegrationProps['handler'] = async (props) => { : wizardResult } catch (thrown: unknown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - return generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) + return oauthWizard.generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) } } diff --git a/integrations/instagram/integration.definition.ts b/integrations/instagram/integration.definition.ts index f43238a99a1..026767c3c38 100644 --- a/integrations/instagram/integration.definition.ts +++ b/integrations/instagram/integration.definition.ts @@ -5,7 +5,7 @@ import proactiveUser from 'bp_modules/proactive-user' import { dmChannelMessages } from './definitions/channel' export const INTEGRATION_NAME = 'instagram' -export const INTEGRATION_VERSION = '4.1.9' +export const INTEGRATION_VERSION = '4.1.10' const commonConfigSchema = z.object({ replyToComments: z diff --git a/integrations/instagram/package.json b/integrations/instagram/package.json index 675d3a82eeb..ee6162ff716 100644 --- a/integrations/instagram/package.json +++ b/integrations/instagram/package.json @@ -13,13 +13,13 @@ "dependencies": { "@botpress/cli": "workspace:*", "@botpress/client": "workspace:*", + "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "@botpress/sdk-addons": "workspace:*", "axios": "^1.6.2" }, "devDependencies": { "@botpress/cli": "workspace:*", - "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "@sentry/cli": "^2.39.1" }, diff --git a/integrations/instagram/src/misc/client.ts b/integrations/instagram/src/misc/client.ts index a43d5a96d3d..9a8ef72f3df 100644 --- a/integrations/instagram/src/misc/client.ts +++ b/integrations/instagram/src/misc/client.ts @@ -1,3 +1,4 @@ +import { handleErrorsDecorator as handleErrors } from '@botpress/common' import { RuntimeError, z } from '@botpress/sdk' import axios from 'axios' import { InstagramRecipientId } from './types' @@ -17,6 +18,7 @@ export class InstagramClient { this._clientId = bp.secrets.CLIENT_ID this._clientSecret = bp.secrets.CLIENT_SECRET } + @handleErrors('Failed to obtain Instagram OAuth access token from authorization code') public async getAccessTokenFromCode(code: string): Promise<{ accessToken: string expirationTime: number @@ -60,6 +62,7 @@ export class InstagramClient { return { accessToken: access_token, expirationTime: Date.now() + expires_in * 1000 } } + @handleErrors('Failed to refresh Instagram OAuth access token') public async refreshAccessToken(): Promise<{ accessToken: string; expirationTime: number }> { const query = new URLSearchParams({ grant_type: 'ig_refresh_token', diff --git a/integrations/instagram/src/webhook/handlers/oauth.ts b/integrations/instagram/src/webhook/handlers/oauth.ts index 1a420f17425..3c68a73be6d 100644 --- a/integrations/instagram/src/webhook/handlers/oauth.ts +++ b/integrations/instagram/src/webhook/handlers/oauth.ts @@ -1,63 +1,58 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import { InstagramClient } from 'src/misc/client' import * as bp from '.botpress' export const oauthCallbackHandler: bp.IntegrationProps['handler'] = async (props: bp.HandlerProps) => { const { client, ctx, req, logger } = props - const queryParams = new URLSearchParams(req.query) - const error = queryParams.get('error') - if (error) { - return { - status: 400, - body: `Error while authenticating with Instagram: ${queryParams.get('error_description') ?? 'Unknown reason'}`, + + try { + const queryParams = new URLSearchParams(req.query) + const error = queryParams.get('error') + if (error) { + throw new Error(`${error} - ${queryParams.get('error_description') ?? ''}`) } - } - const code = queryParams.get('code') - if (!code) { - return { status: 400, body: 'No code found in query parameters' } - } + const code = queryParams.get('code') + if (!code) { + throw new Error('Authorization code not present in OAuth callback') + } - const instagramClient = new InstagramClient(logger) + const instagramClient = new InstagramClient(logger) - const accessTokenInfo = await instagramClient.getAccessTokenFromCode(code).catch(() => undefined) - if (!accessTokenInfo) { - return { status: 500, body: 'Error while getting access token from Instagram' } - } - const { accessToken, expirationTime } = accessTokenInfo - instagramClient.updateAuthConfig({ accessToken }) + const accessTokenInfo = await instagramClient.getAccessTokenFromCode(code) + const { accessToken, expirationTime } = accessTokenInfo + instagramClient.updateAuthConfig({ accessToken }) - const profile = await instagramClient.getUserProfile('me', ['user_id']).catch(() => undefined) - if (!profile) { - return { status: 500, body: 'Error while getting user profile from Instagram' } - } - const instagramId = profile.user_id - instagramClient.updateAuthConfig({ instagramId }) - - const subscribed = await instagramClient - .subscribeToWebhooks(accessToken) - .then(() => true) - .catch(() => false) - if (!subscribed) { - return { status: 500, body: 'Error while subscribing to webhooks' } + const profile = await instagramClient.getUserProfile('me', ['user_id']) + const instagramId = profile.user_id + instagramClient.updateAuthConfig({ instagramId }) + + await instagramClient.subscribeToWebhooks(accessToken) + + await client.setState({ + type: 'integration', + name: 'oauth', + id: ctx.integrationId, + payload: { + accessToken, + instagramId, + expirationTime, + }, + }) + + // Refresh token before 60 days, as indicated in the documentation: + // https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#long-lived + await client.configureIntegration({ + identifier: instagramId, + scheduleRegisterCall: 'monthly', + }) + logger.debug('Token refresh scheduled for Instagram user', instagramId) + await client.updateUser({ id: ctx.botUserId, tags: { id: instagramId } }) + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) } - await client.setState({ - type: 'integration', - name: 'oauth', - id: ctx.integrationId, - payload: { - accessToken, - instagramId, - expirationTime, - }, - }) - - // Refresh token before 60 days, as indicated in the documentation: - // https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#long-lived - await client.configureIntegration({ - identifier: instagramId, - scheduleRegisterCall: 'monthly', - }) - logger.debug('Token refresh scheduled for Instagram user', instagramId) - await client.updateUser({ id: ctx.botUserId, tags: { id: instagramId } }) - return { status: 200 } } diff --git a/integrations/instagram/tsconfig.json b/integrations/instagram/tsconfig.json index d46abc5b88f..32b8ead2675 100644 --- a/integrations/instagram/tsconfig.json +++ b/integrations/instagram/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "paths": { "*": ["./*"] }, - "outDir": "dist" + "outDir": "dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"] } diff --git a/integrations/intercom/integration.definition.ts b/integrations/intercom/integration.definition.ts index c3fbbe20bee..414118cda43 100644 --- a/integrations/intercom/integration.definition.ts +++ b/integrations/intercom/integration.definition.ts @@ -5,7 +5,7 @@ import proactiveUser from 'bp_modules/proactive-user' export default new IntegrationDefinition({ name: 'intercom', - version: '2.0.4', + version: '2.0.5', title: 'Intercom', description: 'Engage with customers in realtime with personalized messaging.', icon: 'icon.svg', diff --git a/integrations/intercom/package.json b/integrations/intercom/package.json index 208ba8f7e83..e4deac76e4c 100644 --- a/integrations/intercom/package.json +++ b/integrations/intercom/package.json @@ -11,6 +11,7 @@ "dependencies": { "@botpress/cli": "workspace:*", "@botpress/client": "workspace:*", + "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "@botpress/sdk-addons": "workspace:*", "axios": "^1.7.8", @@ -18,7 +19,6 @@ }, "devDependencies": { "@botpress/cli": "workspace:*", - "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "@sentry/cli": "^2.39.1" }, diff --git a/integrations/intercom/src/auth.ts b/integrations/intercom/src/auth.ts index 33d1039a7b3..7a767568cfb 100644 --- a/integrations/intercom/src/auth.ts +++ b/integrations/intercom/src/auth.ts @@ -1,3 +1,5 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' +import * as sdk from '@botpress/sdk' import { RuntimeError, z } from '@botpress/sdk' import axios from 'axios' import { Client as IntercomClient } from 'intercom-client' @@ -55,16 +57,32 @@ export const getSignatureSecret = (ctx: bp.Context): string | undefined => { return bp.secrets.CLIENT_SECRET } -export const handleOAuth = async ({ client, ctx, req }: bp.HandlerProps): Promise => { - console.info('Handling OAuth callback') - const code = new URLSearchParams(req.query).get('code') - if (!code) { - throw new RuntimeError('Code not present in OAuth callback request') +export const handleOAuth = async ({ client, ctx, req, logger }: bp.HandlerProps): Promise => { + logger.forBot().info('Handling OAuth callback') + + try { + const searchParams = new URLSearchParams(req.query) + const error = searchParams.get('error') + if (error) { + throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`) + } + + const code = searchParams.get('code') + if (!code) { + throw new Error('Authorization code not present in OAuth callback') + } + + const accessToken = await exchangeCodeForAccessToken(code) + const adminId = await getAdminId(ctx) + await saveAuthCredentials(client, ctx, { accessToken, adminId }) + await client.configureIntegration({ identifier: adminId }) + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) } - const accessToken = await exchangeCodeForAccessToken(code) - const adminId = await getAdminId(ctx) - await saveAuthCredentials(client, ctx, { accessToken, adminId }) - await client.configureIntegration({ identifier: adminId }) } const responseSchema = z.object({ diff --git a/integrations/jira/integration.definition.ts b/integrations/jira/integration.definition.ts index f20b00d326e..031974cdf6b 100644 --- a/integrations/jira/integration.definition.ts +++ b/integrations/jira/integration.definition.ts @@ -7,7 +7,7 @@ export default new IntegrationDefinition({ title: 'Jira', description: 'This integration allows you to work with your Jira workspace, users, projects, and workflow transitions.', - version: '0.5.0', + version: '0.5.1', readme: 'readme.md', icon: 'icon.svg', configuration, diff --git a/integrations/jira/src/oauth-wizard/index.ts b/integrations/jira/src/oauth-wizard/index.ts index 6a77f3708ef..424b8163d2d 100644 --- a/integrations/jira/src/oauth-wizard/index.ts +++ b/integrations/jira/src/oauth-wizard/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { getInterstitialUrl, isOAuthWizardUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/integrations/jira/src/oauth-wizard/wizard.ts b/integrations/jira/src/oauth-wizard/wizard.ts index 0b57f58d39f..6f58aec5df1 100644 --- a/integrations/jira/src/oauth-wizard/wizard.ts +++ b/integrations/jira/src/oauth-wizard/wizard.ts @@ -100,6 +100,14 @@ const _oauthRedirectHandler: WizardHandler = async ({ ctx, client, responses }) const _oauthCallbackHandler: WizardHandler = async ({ ctx, client, logger, responses, query }) => { try { + const error = query.get('error') + if (error) { + return responses.endWizard({ + success: false, + errorMessage: `${error} - ${query.get('error_description') ?? ''}`, + }) + } + const code = query.get('code') if (!code) { return responses.endWizard({ success: false, errorMessage: 'Jira did not return an authorization code' }) diff --git a/integrations/linear/integration.definition.ts b/integrations/linear/integration.definition.ts index 04485c04396..1d10822e7ed 100644 --- a/integrations/linear/integration.definition.ts +++ b/integrations/linear/integration.definition.ts @@ -9,7 +9,7 @@ import listable from './bp_modules/listable' import { actions, channels, events, configuration, configurations, user, states, entities } from './definitions' export const INTEGRATION_NAME = 'linear' -export const INTEGRATION_VERSION = '2.6.0' +export const INTEGRATION_VERSION = '2.6.1' export default new IntegrationDefinition({ name: INTEGRATION_NAME, diff --git a/integrations/linear/package.json b/integrations/linear/package.json index f32a9ae64f4..b732873aa1f 100644 --- a/integrations/linear/package.json +++ b/integrations/linear/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@botpress/client": "workspace:*", + "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "@linear/sdk": "^65.2.0", "axios": "^1.4.0", @@ -17,7 +18,6 @@ }, "devDependencies": { "@botpress/cli": "workspace:*", - "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "@botpresshub/deletable": "workspace:*", "@botpresshub/listable": "workspace:*", diff --git a/integrations/linear/src/handler.ts b/integrations/linear/src/handler.ts index 222f1167be8..fc18ab89ea0 100644 --- a/integrations/linear/src/handler.ts +++ b/integrations/linear/src/handler.ts @@ -1,4 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' +import { generateRedirection } from '@botpress/common/src/oauth-wizard' import * as oauthWizard from '@botpress/common/src/oauth-wizard' import { Request, RuntimeError, OAUTH_IDENTIFIER_HEADER } from '@botpress/sdk' import { LinearWebhookClient } from '@linear/sdk/webhooks' diff --git a/integrations/linear/src/misc/error-handling.ts b/integrations/linear/src/misc/error-handling.ts new file mode 100644 index 00000000000..e55f0c869f3 --- /dev/null +++ b/integrations/linear/src/misc/error-handling.ts @@ -0,0 +1,37 @@ +import { createAsyncFnWrapperWithErrorRedaction, createErrorHandlingDecorator } from '@botpress/common' +import { z, RuntimeError } from '@botpress/sdk' +import axios from 'axios' + +const linearOAuthErrorSchema = z.object({ + error: z.string(), + error_description: z.string().optional(), +}) + +export const redactLinearError = (thrown: unknown, genericErrorMessage: string): RuntimeError => { + const error = thrown instanceof Error ? thrown : new Error(String(thrown)) + + if (error instanceof RuntimeError) { + return error + } + + if (axios.isAxiosError(error)) { + const status = error.response?.status + const data = error.response?.data + const parsed = linearOAuthErrorSchema.safeParse(data) + + if (parsed.success) { + const { error: code, error_description: description } = parsed.data + const detail = description ? `${code}: ${description}` : code + return new RuntimeError(`${genericErrorMessage}: ${detail}${status ? ` (HTTP ${status})` : ''}`) + } + + const fallback = typeof data === 'string' && data.length > 0 ? data : data ? JSON.stringify(data) : error.message + return new RuntimeError(`${genericErrorMessage}: ${fallback}${status ? ` (HTTP ${status})` : ''}`) + } + + return new RuntimeError(`${genericErrorMessage}: ${error.message}`) +} + +export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction(redactLinearError) + +export const handleErrorsDecorator = createErrorHandlingDecorator(wrapAsyncFnWithTryCatch) diff --git a/integrations/linear/src/misc/linear.ts b/integrations/linear/src/misc/linear.ts index 558fb814137..2e36bb3bcef 100644 --- a/integrations/linear/src/misc/linear.ts +++ b/integrations/linear/src/misc/linear.ts @@ -1,6 +1,7 @@ import { RuntimeError, z } from '@botpress/sdk' import { LinearClient } from '@linear/sdk' import axios from 'axios' +import { handleErrorsDecorator as handleErrors } from './error-handling' import { useDeskOAuth } from './utils' import * as bp from '.botpress' @@ -151,6 +152,7 @@ export class LinearOauthClient { } } + @handleErrors('Failed to migrate old Linear OAuth token') public async migrateOldToken(oldAccessToken: string): Promise { const data = await this._handleOAuthRequest( `${linearEndpoint}/oauth/migrate_old_token`, @@ -161,6 +163,7 @@ export class LinearOauthClient { return this._parseCredentials(data) } + @handleErrors('Failed to refresh Linear OAuth access token') public async getAccessTokenFromRefreshToken(oldRefreshToken: string, actor: Actor): Promise { const data = await this._handleOAuthRequest(`${linearEndpoint}/oauth/token`, { grant_type: 'refresh_token', @@ -171,6 +174,7 @@ export class LinearOauthClient { return this._parseCredentials(data) } + @handleErrors('Failed to obtain Linear OAuth access token from authorization code') public async getAccessTokenFromOAuthCode(code: string, actor: Actor) { const data = await this._handleOAuthRequest(`${linearEndpoint}/oauth/token`, { grant_type: 'authorization_code', @@ -184,6 +188,7 @@ export class LinearOauthClient { return this._parseCredentials(data) } + @handleErrors('Failed to resolve valid Linear OAuth credentials') public async resolveValidCredentials(current: Credentials, actor: Actor): Promise { const FIVE_MINUTES_MS = 5 * 60 * 1000 const isExpired = new Date(current.expiresAt).getTime() <= Date.now() + FIVE_MINUTES_MS @@ -195,6 +200,7 @@ export class LinearOauthClient { return current } + @handleErrors('Failed to create Linear client') public static async create(props: { client: bp.Client; ctx: bp.Context }) { return LinearOauthClient._createFromStoredCredentials(props, 'credentials') } diff --git a/integrations/linear/tsconfig.json b/integrations/linear/tsconfig.json index b94d8463ee4..c62e9acfd8f 100644 --- a/integrations/linear/tsconfig.json +++ b/integrations/linear/tsconfig.json @@ -4,7 +4,9 @@ "jsx": "react-jsx", "jsxImportSource": "preact", "paths": { "*": ["./*"] }, - "outDir": "dist" + "outDir": "dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"] } diff --git a/integrations/linkedin/integration.definition.ts b/integrations/linkedin/integration.definition.ts index a6d9abd4702..8e0ad3a0fd6 100644 --- a/integrations/linkedin/integration.definition.ts +++ b/integrations/linkedin/integration.definition.ts @@ -2,7 +2,7 @@ import { IntegrationDefinition } from '@botpress/sdk' import { configuration, configurations, identifier, states, secrets, actions } from './definitions' export const INTEGRATION_NAME = 'linkedin' -export const INTEGRATION_VERSION = '0.1.3' +export const INTEGRATION_VERSION = '0.1.4' export default new IntegrationDefinition({ name: INTEGRATION_NAME, diff --git a/integrations/linkedin/package.json b/integrations/linkedin/package.json index 9ceafb1fc26..10548717690 100644 --- a/integrations/linkedin/package.json +++ b/integrations/linkedin/package.json @@ -12,6 +12,8 @@ "devDependencies": { "@botpress/cli": "workspace:*", "@botpress/client": "workspace:*", - "@botpress/common": "workspace:*" + "@botpress/common": "workspace:*", + "@types/node": "^22.16.4", + "typescript": "^5.6.3" } } diff --git a/integrations/linkedin/src/handler.ts b/integrations/linkedin/src/handler.ts index cfcb96e3491..95d686319da 100644 --- a/integrations/linkedin/src/handler.ts +++ b/integrations/linkedin/src/handler.ts @@ -1,4 +1,4 @@ -import { RuntimeError } from '@botpress/sdk' +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import * as crypto from 'crypto' import { LinkedInOAuthClient } from './linkedin-api' import { verifyLinkedInWebhook, dispatchWebhookEvent } from './webhook' @@ -66,34 +66,37 @@ const handleWebhookChallenge = ({ req, ctx, logger }: bp.HandlerProps) => { const handleOAuthCallback = async ({ req, client, ctx, logger }: bp.HandlerProps) => { logger.forBot().debug('Handling OAuth callback') - const searchParams = new URLSearchParams(req.query) - const authorizationCode = searchParams.get('code') - const error = searchParams.get('error') - const errorDescription = searchParams.get('error_description') + try { + const searchParams = new URLSearchParams(req.query) + const error = searchParams.get('error') + if (error) { + throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`) + } - if (error) { - logger.forBot().error(`LinkedIn OAuth error: ${error} - ${errorDescription}`) - throw new RuntimeError(`LinkedIn OAuth error: ${error} - ${errorDescription}`) - } + const authorizationCode = searchParams.get('code') + if (!authorizationCode) { + throw new Error('Authorization code not present in OAuth callback') + } - if (!authorizationCode) { - logger.forBot().error('Authorization code not present in OAuth callback') - throw new RuntimeError('Authorization code not present in OAuth callback') + const oauthClient = await LinkedInOAuthClient.createFromAuthorizationCode({ + authorizationCode, + client, + ctx, + logger, + }) + + logger.forBot().info(`Successfully authenticated LinkedIn user: ${oauthClient.getUserId()}`) + logger.forBot().info(`Granted scopes: ${oauthClient.getGrantedScopes().join(', ')}`) + + await client.configureIntegration({ + identifier: oauthClient.getUserId(), + }) + + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) } - - const oauthClient = await LinkedInOAuthClient.createFromAuthorizationCode({ - authorizationCode, - client, - ctx, - logger, - }) - - logger.forBot().info(`Successfully authenticated LinkedIn user: ${oauthClient.getUserId()}`) - logger.forBot().info(`Granted scopes: ${oauthClient.getGrantedScopes().join(', ')}`) - - await client.configureIntegration({ - identifier: oauthClient.getUserId(), - }) - - return { status: 200 } } diff --git a/integrations/linkedin/src/linkedin-api/linkedin-oauth-client.ts b/integrations/linkedin/src/linkedin-api/linkedin-oauth-client.ts index fe4ed5f5934..9ad740b8ff3 100644 --- a/integrations/linkedin/src/linkedin-api/linkedin-oauth-client.ts +++ b/integrations/linkedin/src/linkedin-api/linkedin-oauth-client.ts @@ -1,3 +1,4 @@ +import { handleErrorsDecorator as handleErrors } from '@botpress/common' import * as sdk from '@botpress/sdk' import { linkedInErrorResponseSchema, linkedInTokenResponseSchema, userInfoSchema, type UserInfo } from './schemas' import * as bp from '.botpress' @@ -86,6 +87,7 @@ export class LinkedInOAuthClient { * Creates OAuth client using Botpress's official LinkedIn app credentials. * Used for automatic OAuth configuration flow. */ + @handleErrors('Failed to obtain LinkedIn OAuth access token from authorization code') public static async createFromAuthorizationCode({ authorizationCode, client, @@ -107,6 +109,7 @@ export class LinkedInOAuthClient { }) } + @handleErrors('Failed to obtain LinkedIn OAuth access token from manual config') public static async createFromManualConfig({ authorizationCode, clientId, diff --git a/integrations/messenger/integration.definition.ts b/integrations/messenger/integration.definition.ts index 6d22ca4e74f..bf079c57e56 100644 --- a/integrations/messenger/integration.definition.ts +++ b/integrations/messenger/integration.definition.ts @@ -8,7 +8,7 @@ import { actions } from './definitions/actions' import { messages } from './definitions/channels/channel/messages' export const INTEGRATION_NAME = 'messenger' -export const INTEGRATION_VERSION = '5.1.9' +export const INTEGRATION_VERSION = '5.1.10' const commonConfigSchema = z.object({ downloadMedia: z diff --git a/integrations/messenger/src/webhook/handlers/oauth/index.ts b/integrations/messenger/src/webhook/handlers/oauth/index.ts index e354b7505a0..5c22cd8c24a 100644 --- a/integrations/messenger/src/webhook/handlers/oauth/index.ts +++ b/integrations/messenger/src/webhook/handlers/oauth/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import { getErrorFromUnknown } from '../../../misc/utils' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/integrations/notion/integration.definition.ts b/integrations/notion/integration.definition.ts index 91d20971990..5cf64b7b26c 100644 --- a/integrations/notion/integration.definition.ts +++ b/integrations/notion/integration.definition.ts @@ -3,7 +3,7 @@ import filesReadonly from './bp_modules/files-readonly' import { actions, configuration, configurations, events, identifier, secrets, states, user } from './definitions' export const INTEGRATION_NAME = 'notion' -export const INTEGRATION_VERSION = '3.0.5' +export const INTEGRATION_VERSION = '3.0.6' export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, diff --git a/integrations/notion/src/webhook-events/handlers/oauth-callback.ts b/integrations/notion/src/webhook-events/handlers/oauth-callback.ts index 895d23c9bbc..599b9753bee 100644 --- a/integrations/notion/src/webhook-events/handlers/oauth-callback.ts +++ b/integrations/notion/src/webhook-events/handlers/oauth-callback.ts @@ -1,20 +1,33 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import { NotionClient } from '../../notion-api' import * as bp from '.botpress' export const isOAuthCallback = (props: bp.HandlerProps): boolean => props.req.path.startsWith('/oauth') -export const handleOAuthCallback: bp.IntegrationProps['handler'] = async ({ client, ctx, req }) => { - const searchParams = new URLSearchParams(req.query) - const authorizationCode = searchParams.get('code') +export const handleOAuthCallback: bp.IntegrationProps['handler'] = async ({ client, ctx, req, logger }) => { + try { + const searchParams = new URLSearchParams(req.query) + const error = searchParams.get('error') + if (error) { + throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`) + } - if (!authorizationCode) { - console.error('Error extracting code from url') - return - } + const authorizationCode = searchParams.get('code') + if (!authorizationCode) { + throw new Error('Authorization code not present in OAuth callback') + } + + const { workspaceId } = await NotionClient.processAuthorizationCode({ client, ctx }, authorizationCode) - const { workspaceId } = await NotionClient.processAuthorizationCode({ client, ctx }, authorizationCode) + await client.configureIntegration({ + identifier: workspaceId, + }) - await client.configureIntegration({ - identifier: workspaceId, - }) + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) + } } diff --git a/integrations/salesforce/integration.definition.ts b/integrations/salesforce/integration.definition.ts index 5978c9a3e12..cc475ada5b2 100644 --- a/integrations/salesforce/integration.definition.ts +++ b/integrations/salesforce/integration.definition.ts @@ -4,7 +4,7 @@ import { actionDefinitions } from './definitions' export default new IntegrationDefinition({ name: 'salesforce', title: 'Salesforce', - version: '1.0.1', + version: '1.0.2', readme: 'hub.md', icon: 'icon.svg', description: 'Salesforce integration allows you to create, search, update and delete a variety of Salesforce objects', diff --git a/integrations/salesforce/src/handler.ts b/integrations/salesforce/src/handler.ts index 4833d754ea9..288b6128954 100644 --- a/integrations/salesforce/src/handler.ts +++ b/integrations/salesforce/src/handler.ts @@ -1,4 +1,3 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' import * as oauthWizard from '@botpress/common/src/oauth-wizard' import { Connection, OAuth2 } from 'jsforce' import { getEnvironmentUrl } from './misc/utils/sf-utils' @@ -82,6 +81,6 @@ export const handler: bp.IntegrationProps['handler'] = async (props) => { .handleRequest() } catch (thrown: unknown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - return generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) + return oauthWizard.generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) } } diff --git a/integrations/slack/integration.definition.ts b/integrations/slack/integration.definition.ts index 11f59affd74..eeee4db826e 100644 --- a/integrations/slack/integration.definition.ts +++ b/integrations/slack/integration.definition.ts @@ -14,7 +14,7 @@ export default new IntegrationDefinition({ name: 'slack', title: 'Slack', description: 'Automate interactions with your team.', - version: '5.0.2', + version: '5.0.3', icon: 'icon.svg', readme: 'hub.md', configuration: { diff --git a/integrations/slack/src/oauth-wizard/index.ts b/integrations/slack/src/oauth-wizard/index.ts index 95a2a8bd3ac..424b8163d2d 100644 --- a/integrations/slack/src/oauth-wizard/index.ts +++ b/integrations/slack/src/oauth-wizard/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/integrations/stripe/integration.definition.ts b/integrations/stripe/integration.definition.ts index a68f6630c4c..be3ae159d77 100644 --- a/integrations/stripe/integration.definition.ts +++ b/integrations/stripe/integration.definition.ts @@ -34,7 +34,7 @@ import { export default new IntegrationDefinition({ name: 'stripe', - version: '0.6.0', + version: '0.6.1', title: 'Stripe', readme: 'hub.md', icon: 'icon.svg', diff --git a/integrations/stripe/src/oauth-wizard/index.ts b/integrations/stripe/src/oauth-wizard/index.ts index 95a2a8bd3ac..424b8163d2d 100644 --- a/integrations/stripe/src/oauth-wizard/index.ts +++ b/integrations/stripe/src/oauth-wizard/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/integrations/sunco/integration.definition.ts b/integrations/sunco/integration.definition.ts index 66348389437..53788ba83b2 100644 --- a/integrations/sunco/integration.definition.ts +++ b/integrations/sunco/integration.definition.ts @@ -9,7 +9,7 @@ export const INTEGRATION_NAME = 'sunco' export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '2.0.1', + version: '2.0.2', title: 'Sunshine Conversations', description: 'Give your bot access to a powerful omnichannel messaging platform.', icon: 'icon.svg', diff --git a/integrations/sunco/src/handler.ts b/integrations/sunco/src/handler.ts index 5f1ef474c98..916fbf027b0 100644 --- a/integrations/sunco/src/handler.ts +++ b/integrations/sunco/src/handler.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { getWizardStepUrl, isOAuthWizardUrl } from '@botpress/common/src/oauth-wizard' +import { generateRedirection, getWizardStepUrl, isOAuthWizardUrl } from '@botpress/common/src/oauth-wizard' import { RuntimeError } from '@botpress/sdk' import { INTEGRATION_NAME } from '../integration.definition' import { getCredentials } from './api/get-credentials' diff --git a/integrations/todoist/integration.definition.ts b/integrations/todoist/integration.definition.ts index 10a14c7f04e..905d434ac42 100644 --- a/integrations/todoist/integration.definition.ts +++ b/integrations/todoist/integration.definition.ts @@ -16,7 +16,7 @@ export default new sdk.IntegrationDefinition({ name: 'todoist', title: 'Todoist', description: 'Create and modify tasks, post comments and more.', - version: '1.0.5', + version: '1.0.6', readme: 'hub.md', icon: 'icon.svg', actions, diff --git a/integrations/todoist/src/todoist-api/todoist-client.ts b/integrations/todoist/src/todoist-api/todoist-client.ts index eccc62adce1..bd0ac78cb1f 100644 --- a/integrations/todoist/src/todoist-api/todoist-client.ts +++ b/integrations/todoist/src/todoist-api/todoist-client.ts @@ -21,6 +21,7 @@ export class TodoistClient { return new TodoistClient({ accessToken }) } + @handleErrors('Failed to authenticate Todoist with authorization code') public static async authenticateWithAuthorizationCode({ ctx, client, diff --git a/integrations/todoist/src/webhook-events/handler-dispatcher.ts b/integrations/todoist/src/webhook-events/handler-dispatcher.ts index 499da81fa29..a4b170d2533 100644 --- a/integrations/todoist/src/webhook-events/handler-dispatcher.ts +++ b/integrations/todoist/src/webhook-events/handler-dispatcher.ts @@ -29,7 +29,7 @@ export const handler: bp.IntegrationProps['handler'] = async (props: bp.HandlerP } if (!req.body) { - return + return {} } console.debug(req) @@ -38,6 +38,7 @@ export const handler: bp.IntegrationProps['handler'] = async (props: bp.HandlerP await _ensureWebhookIsAuthenticated(props) } await _dispatchEvent(props) + return {} } const _ensureWebhookIsAuthenticated = async ({ req }: bp.HandlerProps) => { diff --git a/integrations/todoist/src/webhook-events/handlers/oauth-callback.ts b/integrations/todoist/src/webhook-events/handlers/oauth-callback.ts index 1cb0596d0b5..45be1b47345 100644 --- a/integrations/todoist/src/webhook-events/handlers/oauth-callback.ts +++ b/integrations/todoist/src/webhook-events/handlers/oauth-callback.ts @@ -1,36 +1,49 @@ +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard/interstitial' import { TodoistClient } from 'src/todoist-api' import * as bp from '.botpress' export const oauthCallbackHandler = async ({ client, ctx, req, logger }: bp.HandlerProps) => { - const searchParams = new URLSearchParams(req.query) - const authorizationCode = searchParams.get('code') - - if (!authorizationCode) { - return + try { + const searchParams = new URLSearchParams(req.query) + const error = searchParams.get('error') + if (error) { + throw new Error(`${error} - ${searchParams.get('error_description') ?? ''}`) + } + + const authorizationCode = searchParams.get('code') + if (!authorizationCode) { + throw new Error('Authorization code not present in OAuth callback') + } + + await TodoistClient.authenticateWithAuthorizationCode({ + client, + ctx, + authorizationCode, + }) + + const todoistClient = await TodoistClient.create({ client, ctx }) + + const userIdentity = await todoistClient.getAuthenticatedUserIdentity() + + await client.configureIntegration({ + identifier: userIdentity.id, + }) + + await client.updateUser({ + id: ctx.botUserId, + name: userIdentity.name, + pictureUrl: userIdentity.pictureUrl, + tags: { + id: userIdentity.id, + }, + }) + + logger.forBot().info('Successfully authenticated with Todoist') + return generateRedirection(getInterstitialUrl(true)) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const errorMessage = 'OAuth error: ' + msg + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) } - - await TodoistClient.authenticateWithAuthorizationCode({ - client, - ctx, - authorizationCode, - }) - - const todoistClient = await TodoistClient.create({ client, ctx }) - - const userIdentity = await todoistClient.getAuthenticatedUserIdentity() - - await client.configureIntegration({ - identifier: userIdentity.id, - }) - - await client.updateUser({ - id: ctx.botUserId, - name: userIdentity.name, - pictureUrl: userIdentity.pictureUrl, - tags: { - id: userIdentity.id, - }, - }) - - logger.forBot().info('Successfully authenticated with Todoist') } diff --git a/integrations/whatsapp/integration.definition.ts b/integrations/whatsapp/integration.definition.ts index 7c6a017cfd5..de577d9ee44 100644 --- a/integrations/whatsapp/integration.definition.ts +++ b/integrations/whatsapp/integration.definition.ts @@ -157,7 +157,7 @@ const defaultBotPhoneNumberId = { } export const INTEGRATION_NAME = 'whatsapp' -export const INTEGRATION_VERSION = '4.16.1' +export const INTEGRATION_VERSION = '4.16.2' export default new IntegrationDefinition({ name: INTEGRATION_NAME, version: INTEGRATION_VERSION, diff --git a/integrations/whatsapp/src/webhook/handlers/oauth/index.ts b/integrations/whatsapp/src/webhook/handlers/oauth/index.ts index 58909ec171e..27237b6829b 100644 --- a/integrations/whatsapp/src/webhook/handlers/oauth/index.ts +++ b/integrations/whatsapp/src/webhook/handlers/oauth/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/integrations/zendesk-messaging-hitl/integration.definition.ts b/integrations/zendesk-messaging-hitl/integration.definition.ts index 9b342b3620c..9e827e1ec5e 100644 --- a/integrations/zendesk-messaging-hitl/integration.definition.ts +++ b/integrations/zendesk-messaging-hitl/integration.definition.ts @@ -6,7 +6,7 @@ export const INTEGRATION_NAME = 'zendesk-messaging-hitl' export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, - version: '1.1.0', + version: '1.1.1', title: 'Zendesk Messaging HITL', description: 'This integration allows your bot to use Sunshine Conversations (Sunco) as a HITL Provider for Zendesk', icon: 'icon.svg', diff --git a/integrations/zendesk-messaging-hitl/src/handler.ts b/integrations/zendesk-messaging-hitl/src/handler.ts index f087d7be737..333191ff324 100644 --- a/integrations/zendesk-messaging-hitl/src/handler.ts +++ b/integrations/zendesk-messaging-hitl/src/handler.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { generateRedirection, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' import { RuntimeError } from '@botpress/sdk' import { INTEGRATION_NAME } from './../integration.definition' import { getCredentials } from './api/get-credentials' diff --git a/integrations/zendesk/integration.definition.ts b/integrations/zendesk/integration.definition.ts index a220711988e..5b1fc5e0355 100644 --- a/integrations/zendesk/integration.definition.ts +++ b/integrations/zendesk/integration.definition.ts @@ -6,7 +6,7 @@ import { actions, events, configuration, channels, states, user } from './src/de export default new sdk.IntegrationDefinition({ name: 'zendesk', title: 'Zendesk', - version: '3.1.5', + version: '3.1.6', icon: 'icon.svg', description: 'Optimize your support workflow. Trigger workflows from ticket updates as well as manage tickets, access conversations, and engage with customers.', diff --git a/integrations/zendesk/src/oauth/index.ts b/integrations/zendesk/src/oauth/index.ts index 58909ec171e..27237b6829b 100644 --- a/integrations/zendesk/src/oauth/index.ts +++ b/integrations/zendesk/src/oauth/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/integrations/zoho/integration.definition.ts b/integrations/zoho/integration.definition.ts index a09edec0f3f..ddf7acc70c0 100644 --- a/integrations/zoho/integration.definition.ts +++ b/integrations/zoho/integration.definition.ts @@ -40,7 +40,7 @@ import { zohoCredentialsStateSchema, zohoOAuthWizardStateSchema } from './src/mi export default new IntegrationDefinition({ name: 'zoho', - version: '4.0.0', + version: '4.0.1', title: 'Zoho', readme: 'hub.md', icon: 'icon.svg', diff --git a/integrations/zoho/src/oauth-wizard/index.ts b/integrations/zoho/src/oauth-wizard/index.ts index cf886eb0694..8f7286382d9 100644 --- a/integrations/zoho/src/oauth-wizard/index.ts +++ b/integrations/zoho/src/oauth-wizard/index.ts @@ -1,5 +1,4 @@ -import { generateRedirection } from '@botpress/common/src/html-dialogs' -import { getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import { getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard' import * as wizard from './wizard' import * as bp from '.botpress' diff --git a/packages/common/src/error-handling/try-catch-wrapper.ts b/packages/common/src/error-handling/try-catch-wrapper.ts index a26230656df..6fec520d447 100644 --- a/packages/common/src/error-handling/try-catch-wrapper.ts +++ b/packages/common/src/error-handling/try-catch-wrapper.ts @@ -130,3 +130,15 @@ export const createErrorHandlingDecorator = return asyncFnWrapperWithErrorRedaction(_originalMethod.bind(this), errorMessage).apply(this, args) } } + +/** + * Default async function wrapper that uses {@link defaultErrorRedactor} to + * convert thrown errors into `sdk.RuntimeError` instances with a custom message. + */ +export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction(defaultErrorRedactor) + +/** + * Default class method decorator that wraps the method with the default + * try-catch wrapper. + */ +export const handleErrorsDecorator = createErrorHandlingDecorator(wrapAsyncFnWithTryCatch) diff --git a/packages/common/src/html-dialogs/index.ts b/packages/common/src/html-dialogs/index.ts index 37614f99050..b149efb6b7b 100644 --- a/packages/common/src/html-dialogs/index.ts +++ b/packages/common/src/html-dialogs/index.ts @@ -4,14 +4,6 @@ import * as preact from 'preact-render-to-string' import { DISABLE_INTERSTITIAL_HEADER } from '../oauth-wizard' import { ButtonDialogPage, SelectDialogPage, InputDialogPage, FormDialogPage } from './components' -export const generateRedirection = (url: URL): sdk.Response => ({ - status: 303, - headers: { - ...DISABLE_INTERSTITIAL_HEADER, - location: url.toString(), - }, -}) - type CommonDialogProps = { pageTitle: string } diff --git a/packages/common/src/oauth-wizard/index.ts b/packages/common/src/oauth-wizard/index.ts index 7d3803e537a..b13e0accecd 100644 --- a/packages/common/src/oauth-wizard/index.ts +++ b/packages/common/src/oauth-wizard/index.ts @@ -1,5 +1,6 @@ export { OAuthWizardBuilder } from './wizard-builder' -export { getWizardStepUrl, isOAuthWizardUrl, getInterstitialUrl } from './wizard-handler' +export { getWizardStepUrl, isOAuthWizardUrl } from './wizard-handler' +export { getInterstitialUrl, generateRedirection } from './interstitial' export { CHOICE_PARAM, DISABLE_INTERSTITIAL_HEADER, FORM_PARAM_PREFIX } from './consts' export { schemaToFieldDescriptors } from './schema-to-fields' export type { FormFieldDescriptor } from './schema-to-fields' diff --git a/packages/common/src/oauth-wizard/interstitial.ts b/packages/common/src/oauth-wizard/interstitial.ts new file mode 100644 index 00000000000..afe31c87f5a --- /dev/null +++ b/packages/common/src/oauth-wizard/interstitial.ts @@ -0,0 +1,16 @@ +import { Response } from '@botpress/sdk' +import { DISABLE_INTERSTITIAL_HEADER } from './consts' + +export const getInterstitialUrl = (success: boolean, message?: string) => + new URL( + process.env.BP_WEBHOOK_URL?.replace('webhook', 'app') + + `/oauth/interstitial?success=${success}${message ? `&errorMessage=${encodeURIComponent(message)}` : ''}` + ) + +export const generateRedirection = (url: URL): Response => ({ + status: 303, + headers: { + ...DISABLE_INTERSTITIAL_HEADER, + location: url.toString(), + }, +}) diff --git a/packages/common/src/oauth-wizard/wizard-handler.ts b/packages/common/src/oauth-wizard/wizard-handler.ts index 2908c215fab..093aa6fbc4c 100644 --- a/packages/common/src/oauth-wizard/wizard-handler.ts +++ b/packages/common/src/oauth-wizard/wizard-handler.ts @@ -1,7 +1,8 @@ -import * as sdk from '@botpress/sdk' +import { Response, RuntimeError, z, OAUTH_IDENTIFIER_HEADER } from '@botpress/sdk' import * as preact from 'preact-render-to-string' import * as htmlDialogs from '../html-dialogs' import * as consts from './consts' +import { generateRedirection, getInterstitialUrl } from './interstitial' import { schemaToFieldDescriptors } from './schema-to-fields' import type * as types from './types' @@ -20,9 +21,9 @@ export class OAuthWizard { this._handlerProps = handlerProps } - public async handleRequest(): Promise { + public async handleRequest(): Promise { if (!isOAuthWizardUrl(this._handlerProps.req.path)) { - throw new sdk.RuntimeError('Invalid OAuth wizard URL') + throw new RuntimeError('Invalid OAuth wizard URL') } const searchParams = new URLSearchParams(this._handlerProps.req.query) @@ -30,7 +31,7 @@ export class OAuthWizard { const step = this._steps.get(stepId) if (!step) { - throw new sdk.RuntimeError(`Unknown step ID: ${stepId}`) + throw new RuntimeError(`Unknown step ID: ${stepId}`) } const formValues: Record = {} @@ -44,18 +45,18 @@ export class OAuthWizard { const rawSchemaJson = formParams.get(consts.FORM_SCHEMA_PARAM) if (rawSchemaJson && Object.keys(formValues).length > 0) { const jsonSchema = JSON.parse(rawSchemaJson) as { properties?: Record } - const coercedShape: Record = {} + const coercedShape: Record = {} for (const name of Object.keys(formValues)) { const fieldType = jsonSchema.properties?.[name]?.type if (fieldType === 'boolean') { - coercedShape[name] = sdk.z.coerce.boolean() + coercedShape[name] = z.coerce.boolean() } else if (fieldType === 'number' || fieldType === 'integer') { - coercedShape[name] = sdk.z.coerce.number() + coercedShape[name] = z.coerce.number() } else { - coercedShape[name] = sdk.z.coerce.string() + coercedShape[name] = z.coerce.string() } } - const coerced = sdk.z.object(coercedShape).safeParse(formValues) + const coerced = z.object(coercedShape).safeParse(formValues) if (coerced.success) { Object.assign(formValues, coerced.data) } @@ -72,7 +73,7 @@ export class OAuthWizard { inputValue: searchParams.get(consts.INPUT_PARAM) ?? undefined, formValues: Object.keys(formValues).length > 0 ? formValues : undefined, setIntegrationIdentifier(identifier: string) { - extraHeaders[sdk.OAUTH_IDENTIFIER_HEADER] = identifier + extraHeaders[OAUTH_IDENTIFIER_HEADER] = identifier }, responses: { displayButtons: ({ buttons, pageTitle, htmlOrMarkdownPageContents }) => @@ -138,12 +139,10 @@ export class OAuthWizard { pageTitle, bodyHtml: preact.render(body), }), - redirectToStep: (stepId) => htmlDialogs.generateRedirection(getWizardStepUrl(stepId, this._handlerProps.ctx)), - redirectToExternalUrl: (url) => htmlDialogs.generateRedirection(new URL(url)), + redirectToStep: (stepId) => generateRedirection(getWizardStepUrl(stepId, this._handlerProps.ctx)), + redirectToExternalUrl: (url) => generateRedirection(new URL(url)), endWizard: (result) => - htmlDialogs.generateRedirection( - getInterstitialUrl(result.success, result.success ? undefined : result.errorMessage) - ), + generateRedirection(getInterstitialUrl(result.success, result.success ? undefined : result.errorMessage)), }, }) @@ -162,9 +161,3 @@ export const getWizardStepUrl = (stepId: string, ctx?: { webhookId: string }): U export const isOAuthWizardUrl = (path: string): path is (typeof consts)['BASE_WIZARD_PATH'] => path.startsWith(consts.BASE_WIZARD_PATH) - -export const getInterstitialUrl = (success: boolean, message?: string) => - new URL( - process.env.BP_WEBHOOK_URL?.replace('webhook', 'app') + - `/oauth/interstitial?success=${success}${message ? `&errorMessage=${message}` : ''}` - ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d0cc8f50bc..8765e802ce0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -615,6 +615,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 @@ -836,6 +839,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 @@ -1356,6 +1362,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 @@ -1366,9 +1375,6 @@ importers: specifier: ^1.6.2 version: 1.7.4 devDependencies: - '@botpress/common': - specifier: workspace:* - version: link:../../packages/common '@sentry/cli': specifier: ^2.39.1 version: 2.39.1 @@ -1381,6 +1387,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 @@ -1394,9 +1403,6 @@ importers: specifier: ^4.0.0 version: 4.0.0 devDependencies: - '@botpress/common': - specifier: workspace:* - version: link:../../packages/common '@sentry/cli': specifier: ^2.39.1 version: 2.39.1 @@ -1487,6 +1493,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 @@ -1506,9 +1515,6 @@ importers: '@botpress/cli': specifier: workspace:* version: link:../../packages/cli - '@botpress/common': - specifier: workspace:* - version: link:../../packages/common '@botpresshub/deletable': specifier: workspace:* version: link:../../interfaces/deletable @@ -1534,6 +1540,12 @@ importers: '@botpress/common': specifier: workspace:* version: link:../../packages/common + '@types/node': + specifier: ^22.16.4 + version: 22.16.4 + typescript: + specifier: ^5.6.3 + version: 5.9.3 integrations/loops: dependencies: