diff --git a/integrations/asana/integration.definition.ts b/integrations/asana/integration.definition.ts index 42180a9815c..72c8f0086f3 100644 --- a/integrations/asana/integration.definition.ts +++ b/integrations/asana/integration.definition.ts @@ -7,7 +7,7 @@ import { configuration, states, user, channels, actions } from './src/definition export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '0.3.8', + version: '0.3.9', title: 'Asana', readme: 'hub.md', description: 'Connect your bot to your Asana inbox, create and update tasks, add comments, and locate users.', diff --git a/integrations/asana/src/definitions/channels.ts b/integrations/asana/src/definitions/channels.ts index 3d46c035824..7ee9ad0440f 100644 --- a/integrations/asana/src/definitions/channels.ts +++ b/integrations/asana/src/definitions/channels.ts @@ -7,6 +7,7 @@ export const channels = { messages: { ...messages.defaults, markdown: messages.markdown, + bloc: messages.markdownBloc, }, }, } satisfies IntegrationDefinitionProps['channels'] diff --git a/integrations/chat/definitions/channels/messages.ts b/integrations/chat/definitions/channels/messages.ts index b62a85c3582..3fbfc2600eb 100644 --- a/integrations/chat/definitions/channels/messages.ts +++ b/integrations/chat/definitions/channels/messages.ts @@ -2,6 +2,7 @@ import * as sdk from '@botpress/sdk' import { z } from '@botpress/sdk' const metadata = z.record(z.any()).optional() + const text = { schema: sdk.messages.defaults.text.schema.extend({ metadata }) } const image = { schema: sdk.messages.defaults.image.schema.extend({ metadata }) } const audio = { schema: sdk.messages.defaults.audio.schema.extend({ metadata }) } @@ -12,7 +13,8 @@ const carousel = { schema: sdk.messages.defaults.carousel.schema.extend({ metada const card = { schema: sdk.messages.defaults.card.schema.extend({ metadata }) } const dropdown = { schema: sdk.messages.defaults.dropdown.schema.extend({ metadata }) } const choice = { schema: sdk.messages.defaults.choice.schema.extend({ metadata }) } -const bloc = { schema: sdk.messages.defaults.bloc.schema.extend({ metadata }) } + +const bloc = { schema: sdk.messages.markdownBloc.schema.extend({ metadata }) } const markdown = { schema: sdk.messages.markdown.schema.extend({ metadata }) } export const messages = { diff --git a/integrations/freshchat/integration.definition.ts b/integrations/freshchat/integration.definition.ts index 2c176b89b85..169bbedeb98 100644 --- a/integrations/freshchat/integration.definition.ts +++ b/integrations/freshchat/integration.definition.ts @@ -6,7 +6,7 @@ import { events, configuration, channels, states, user } from './src/definitions export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, title: 'Freshchat', - version: '1.5.2', + version: '1.5.3', icon: 'icon.svg', description: 'This integration allows your bot to use Freshchat as a HITL Provider', readme: 'hub.md', diff --git a/integrations/gmail/definitions/channels.ts b/integrations/gmail/definitions/channels.ts index 02d64a6a521..ad853f03698 100644 --- a/integrations/gmail/definitions/channels.ts +++ b/integrations/gmail/definitions/channels.ts @@ -27,6 +27,9 @@ export const channels = { title: sdk.z.string().optional().title('Title').describe('Title for the file'), }), }, + bloc: { + schema: sdk.messages.markdownBloc.schema, + }, }, message: { tags: { diff --git a/integrations/gmail/integration.definition.ts b/integrations/gmail/integration.definition.ts index 4998fbfd432..e362061594d 100644 --- a/integrations/gmail/integration.definition.ts +++ b/integrations/gmail/integration.definition.ts @@ -13,7 +13,7 @@ import { export default new sdk.IntegrationDefinition({ name: 'gmail', - version: '0.6.4', + version: '0.6.5', title: 'Gmail', description: "Send, receive, and manage emails directly within your bot's workflow.", icon: 'icon.svg', diff --git a/integrations/googlecalendar/integration.definition.ts b/integrations/googlecalendar/integration.definition.ts index e76097831c4..dd17191374c 100644 --- a/integrations/googlecalendar/integration.definition.ts +++ b/integrations/googlecalendar/integration.definition.ts @@ -2,10 +2,11 @@ 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.3' export default new sdk.IntegrationDefinition({ - name: 'googlecalendar', - version: '2.0.2', + name: INTEGRATION_NAME, + version: INTEGRATION_VERSION, description: 'Sync with your calendar to manage events, appointments, and schedules directly within the chatbot.', title: 'Google Calendar', readme: 'hub.md', diff --git a/integrations/googlecalendar/src/google-api/error-handling.ts b/integrations/googlecalendar/src/google-api/error-handling.ts index 294ea47b321..95ebe77ddd9 100644 --- a/integrations/googlecalendar/src/google-api/error-handling.ts +++ b/integrations/googlecalendar/src/google-api/error-handling.ts @@ -2,7 +2,7 @@ import { isApiError } from '@botpress/client' import { createAsyncFnWrapperWithErrorRedaction, createErrorHandlingDecorator, posthogHelper } from '@botpress/common' import * as sdk from '@botpress/sdk' import { Common as GoogleApisCommon } from 'googleapis' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import * as bp from '.botpress' export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction((error: Error, customMessage: string) => { @@ -31,7 +31,11 @@ export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction((e errorReason: errorReason?.substring(0, 100), }, }, - { integrationName: INTEGRATION_NAME, key: (bp.secrets as any).POSTHOG_KEY as string } + { + integrationName: INTEGRATION_NAME, + integrationVersion: INTEGRATION_VERSION, + key: (bp.secrets as any).POSTHOG_KEY as string, + } ) .catch(() => { // Silently fail if PostHog is unavailable diff --git a/integrations/googlecalendar/src/index.ts b/integrations/googlecalendar/src/index.ts index efe65a49719..413a0357d0b 100644 --- a/integrations/googlecalendar/src/index.ts +++ b/integrations/googlecalendar/src/index.ts @@ -1,6 +1,6 @@ import { posthogHelper } from '@botpress/common' import { sentry as sentryHelpers } from '@botpress/sdk-addons' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import { actions } from './actions' import { register, unregister } from './setup' import { handler } from './webhook-events' @@ -8,6 +8,7 @@ import * as bp from '.botpress' @posthogHelper.wrapIntegration({ integrationName: INTEGRATION_NAME, + integrationVersion: INTEGRATION_VERSION, key: (bp.secrets as any).POSTHOG_KEY as string, }) class GoogleCalendarIntegration extends bp.Integration { diff --git a/integrations/instagram/definitions/channel.ts b/integrations/instagram/definitions/channel.ts index f508a4230a5..db677c9ec0e 100644 --- a/integrations/instagram/definitions/channel.ts +++ b/integrations/instagram/definitions/channel.ts @@ -20,8 +20,8 @@ const channelMessages = { card: { schema: sdk.messages.defaults.card.schema.merge(commentIdSchema) }, dropdown: { schema: sdk.messages.defaults.dropdown.schema.merge(commentIdSchema) }, choice: { schema: sdk.messages.defaults.choice.schema.merge(commentIdSchema) }, - bloc: { schema: sdk.messages.defaults.bloc.schema.merge(commentIdSchema) }, -} as const satisfies typeof sdk.messages.defaults + bloc: { schema: sdk.messages.markdownBloc.schema.merge(commentIdSchema) }, +} as const satisfies Record type ValueOf = T[keyof T] type ChannelMessageDefinition = ValueOf diff --git a/integrations/instagram/integration.definition.ts b/integrations/instagram/integration.definition.ts index 507381c6d84..7d634c08ba3 100644 --- a/integrations/instagram/integration.definition.ts +++ b/integrations/instagram/integration.definition.ts @@ -5,6 +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.4' const commonConfigSchema = z.object({ replyToComments: z @@ -16,7 +17,7 @@ const commonConfigSchema = z.object({ export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '4.1.2', + version: INTEGRATION_VERSION, title: 'Instagram', description: 'Automate interactions, manage comments, and send/receive messages all in real-time.', icon: 'icon.svg', diff --git a/integrations/instagram/src/index.ts b/integrations/instagram/src/index.ts index ba292af2e35..6c3ae761451 100644 --- a/integrations/instagram/src/index.ts +++ b/integrations/instagram/src/index.ts @@ -1,5 +1,5 @@ import { posthogHelper } from '@botpress/common' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import actions from './actions' import channels from './channels' import { register, unregister } from './setup' @@ -8,6 +8,7 @@ import * as bp from '.botpress' @posthogHelper.wrapIntegration({ integrationName: INTEGRATION_NAME, + integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY, }) class InstagramIntegration extends bp.Integration { diff --git a/integrations/intercom/integration.definition.ts b/integrations/intercom/integration.definition.ts index 4a882accfe0..86c703203f7 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.0', + version: '2.0.1', title: 'Intercom', description: 'Engage with customers in realtime with personalized messaging.', icon: 'icon.svg', @@ -53,7 +53,7 @@ export default new IntegrationDefinition({ channel: { title: 'Intercom conversation', description: 'Channel for a Intercom conversation', - messages: messages.defaults, + messages: { ...messages.defaults, bloc: messages.markdownBloc }, message: { tags: { id: { diff --git a/integrations/line/integration.definition.ts b/integrations/line/integration.definition.ts index 5707a38dcf2..4d8723a65d5 100644 --- a/integrations/line/integration.definition.ts +++ b/integrations/line/integration.definition.ts @@ -6,7 +6,7 @@ import typingIndicator from 'bp_modules/typing-indicator' export default new IntegrationDefinition({ name: 'line', - version: '2.0.2', + version: '2.0.3', title: 'Line', description: 'Interact with customers using a rich set of features.', icon: 'icon.svg', @@ -29,7 +29,7 @@ export default new IntegrationDefinition({ channel: { title: 'Line conversation', description: 'Channel for a Line conversation', - messages: { ...messages.defaults }, + messages: { ...messages.defaults, bloc: messages.markdownBloc }, message: { tags: { msgId: { diff --git a/integrations/linear/definitions/index.ts b/integrations/linear/definitions/index.ts index 0751e3246a5..a1b18c1d660 100644 --- a/integrations/linear/definitions/index.ts +++ b/integrations/linear/definitions/index.ts @@ -35,7 +35,7 @@ export const channels = { issue: { title: 'Issue', description: 'A linear issue', - messages: { ...messages.defaults, markdown: messages.markdown }, + messages: { ...messages.defaults, markdown: messages.markdown, bloc: messages.markdownBloc }, message: { tags: { id: { diff --git a/integrations/linear/integration.definition.ts b/integrations/linear/integration.definition.ts index a18764ce74f..82071609dff 100644 --- a/integrations/linear/integration.definition.ts +++ b/integrations/linear/integration.definition.ts @@ -6,7 +6,7 @@ import { actions, channels, events, configuration, configurations, user, states, export default new IntegrationDefinition({ name: 'linear', - version: '1.1.4', + version: '1.1.5', title: 'Linear', description: 'Manage your projects autonomously. Have your bot participate in discussions, manage issues and teams, and track progress.', diff --git a/integrations/linear/src/handler.ts b/integrations/linear/src/handler.ts index f4a08ed643b..6d590fdd76c 100644 --- a/integrations/linear/src/handler.ts +++ b/integrations/linear/src/handler.ts @@ -5,6 +5,7 @@ import { fireIssueCreated } from './events/issueCreated' import { fireIssueDeleted } from './events/issueDeleted' import { fireIssueUpdated } from './events/issueUpdated' import { LinearEvent, handleOauth } from './misc/linear' +import { Result } from './misc/types' import { getLinearClient, getUserAndConversation } from './misc/utils' import * as bp from '.botpress' @@ -23,13 +24,11 @@ export const handler: bp.IntegrationProps['handler'] = async ({ req, ctx, client const linearEvent: LinearEvent = JSON.parse(req.body) linearEvent.type = linearEvent.type.toLowerCase() as LinearEvent['type'] - if (!_isWebhookProperlyAuthenticated({ req, linearEvent, ctx })) { - logger - .forBot() - .error( - 'Received a webhook event that is not properly authenticated. Please ensure the webhook signing secret is correct.' - ) - throw new Error('Webhook event is not properly authenticated: the signing secret is invalid.') + const result = _safeCheckWebhookSignature({ req, linearEvent, ctx }) + if (!result.success) { + const message = `Error while verifying webhook signature: ${result.message}` + logger.forBot().error(message) + throw new Error(message) } const linearBotId = await _getLinearBotId({ client, ctx }) @@ -91,7 +90,7 @@ export const handler: bp.IntegrationProps['handler'] = async ({ req, ctx, client } } -const _isWebhookProperlyAuthenticated = ({ +const _safeCheckWebhookSignature = ({ req, linearEvent, ctx, @@ -99,18 +98,29 @@ const _isWebhookProperlyAuthenticated = ({ req: Request linearEvent: LinearEvent ctx: bp.Context -}) => { +}): Result => { const webhookSignatureHeader = req.headers[LINEAR_WEBHOOK_SIGNATURE_HEADER] if (!webhookSignatureHeader || !req.body) { - return + return { success: false, message: 'missing signature header or request body' } } const webhookHandler = new LinearWebhooks(_getWebhookSigningSecret({ ctx })) const bodyBuffer = Buffer.from(req.body) const timeStampHeader = linearEvent[LINEAR_WEBHOOK_TS_FIELD] - - return webhookHandler.verify(bodyBuffer, webhookSignatureHeader, timeStampHeader) + try { + const result = webhookHandler.verify(bodyBuffer, webhookSignatureHeader, timeStampHeader) + if (result) { + return { success: true, result: undefined } + } + return { success: false, message: 'webhook signature verification failed' } + } catch (thrown) { + const errorMessage = thrown instanceof Error ? thrown.message : String(thrown) + return { + success: false, + message: `Webhook signature verification failed: ${errorMessage}`, + } + } } const _getWebhookSigningSecret = ({ ctx }: { ctx: bp.Context }) => diff --git a/integrations/linear/src/misc/types.ts b/integrations/linear/src/misc/types.ts index e8680418dcb..f8cac7f3db0 100644 --- a/integrations/linear/src/misc/types.ts +++ b/integrations/linear/src/misc/types.ts @@ -20,3 +20,13 @@ export type MessageDefinition = sdk.MessageDefinition export type ActionProps = bp.AnyActionProps export type MessageHandlerProps = bp.AnyMessageProps export type AckFunction = bp.AnyAckFunction + +export type Result = + | { + success: true + result: T + } + | { + success: false + message: string + } diff --git a/integrations/messenger/definitions/channels/channel/messages.ts b/integrations/messenger/definitions/channels/channel/messages.ts index 1323ac9c197..0b8e2caf636 100644 --- a/integrations/messenger/definitions/channels/channel/messages.ts +++ b/integrations/messenger/definitions/channels/channel/messages.ts @@ -20,8 +20,8 @@ export const messages = { card: { schema: sdk.messages.defaults.card.schema.merge(commentIdSchema) }, dropdown: { schema: sdk.messages.defaults.dropdown.schema.merge(commentIdSchema) }, choice: { schema: sdk.messages.defaults.choice.schema.merge(commentIdSchema) }, - bloc: { schema: sdk.messages.defaults.bloc.schema.merge(commentIdSchema) }, -} as const satisfies typeof sdk.messages.defaults + bloc: { schema: sdk.messages.markdownBloc.schema.merge(commentIdSchema) }, +} as const satisfies Record type ValueOf = T[keyof T] type ChannelMessageDefinition = ValueOf diff --git a/integrations/messenger/integration.definition.ts b/integrations/messenger/integration.definition.ts index 0b6be4e864e..f7c7cc9a78e 100644 --- a/integrations/messenger/integration.definition.ts +++ b/integrations/messenger/integration.definition.ts @@ -7,6 +7,7 @@ import typingIndicator from 'bp_modules/typing-indicator' import { messages } from './definitions/channels/channel/messages' export const INTEGRATION_NAME = 'messenger' +export const INTEGRATION_VERSION = '5.0.4' const commonConfigSchema = z.object({ downloadMedia: z @@ -36,7 +37,7 @@ const replyToCommentsSchema = z.object({ export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '5.0.2', + version: INTEGRATION_VERSION, title: 'Messenger and Facebook', description: 'Give your bot access to one of the world’s largest messaging platforms and manage your Facebook page content in one place.', diff --git a/integrations/messenger/src/channels/channel.ts b/integrations/messenger/src/channels/channel.ts index 09d0219aa71..21b2c8b95ec 100644 --- a/integrations/messenger/src/channels/channel.ts +++ b/integrations/messenger/src/channels/channel.ts @@ -111,9 +111,16 @@ async function _sendMessage( .debug( `Sending ${type} message ${commentId ? 'as private reply ' : ''}from bot to Messenger: ${_formatPayloadToStr(payload)}` ) - const messengerClient = await createAuthenticatedMessengerClient(client, ctx) - const { messageId } = await send(messengerClient, recipient) - await ack({ tags: { id: messageId, commentId } }) + + try { + const messengerClient = await createAuthenticatedMessengerClient(client, ctx) + const { messageId } = await send(messengerClient, recipient) + await ack({ tags: { id: messageId, commentId } }) + } catch (thrown) { + const error = thrown instanceof Error ? thrown : new Error(String(thrown)) + const errorMessage = `Failed to send ${type} message to Messenger: ${error.message}` + logger.forBot().error(errorMessage) + } if (commentId && conversation.tags.lastCommentId !== commentId) { await client.updateConversation({ diff --git a/integrations/messenger/src/index.ts b/integrations/messenger/src/index.ts index 667f872bfc4..61e7e964a8a 100644 --- a/integrations/messenger/src/index.ts +++ b/integrations/messenger/src/index.ts @@ -1,5 +1,5 @@ import { posthogHelper } from '@botpress/common' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import actions from './actions' import channels from './channels' import { register, unregister } from './setup' @@ -8,6 +8,7 @@ import * as bp from '.botpress' @posthogHelper.wrapIntegration({ integrationName: INTEGRATION_NAME, + integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY, }) class MessengerIntegration extends bp.Integration { diff --git a/integrations/messenger/src/webhook/handler.ts b/integrations/messenger/src/webhook/handler.ts index 0da69ec4c75..5bc073809a0 100644 --- a/integrations/messenger/src/webhook/handler.ts +++ b/integrations/messenger/src/webhook/handler.ts @@ -59,14 +59,15 @@ const _handler: bp.IntegrationProps['handler'] = async (props) => { const _handlerWrapper: typeof _handler = async (props: bp.HandlerProps) => { try { const response = await _handler(props) + if (response?.status && response.status >= 400) { - const errorMessage = `Messenger handler failed with status ${response.status}: ${response.body}` - props.logger.error(errorMessage) + throw new Error(`${response.status}: ${response.body}`) } + return response } catch (thrown: unknown) { - const errorMsg = thrown instanceof Error ? thrown.message : String(thrown) - const errorMessage = `Messenger handler failed with error: ${errorMsg}` + const error = thrown instanceof Error ? thrown.message : String(thrown) + const errorMessage = `Messenger handler failed with error: ${error}` props.logger.error(errorMessage) return { status: 500, body: errorMessage } } diff --git a/integrations/slack/definitions/channels/channels.ts b/integrations/slack/definitions/channels/channels.ts index 60e3c89bf26..ebd2a7ffb64 100644 --- a/integrations/slack/definitions/channels/channels.ts +++ b/integrations/slack/definitions/channels/channels.ts @@ -6,6 +6,7 @@ const messages = { text: { schema: textSchema, }, + bloc: sdk.messages.markdownBloc, } as const satisfies sdk.ChannelDefinition['messages'] const conversationTags = { diff --git a/integrations/slack/integration.definition.ts b/integrations/slack/integration.definition.ts index cdd609a62c1..643a3a26d42 100644 --- a/integrations/slack/integration.definition.ts +++ b/integrations/slack/integration.definition.ts @@ -17,7 +17,7 @@ export default new IntegrationDefinition({ name: 'slack', title: 'Slack', description: 'Automate interactions with your team.', - version: '3.1.1', + version: '3.1.2', icon: 'icon.svg', readme: 'hub.md', configuration, diff --git a/integrations/sunco/integration.definition.ts b/integrations/sunco/integration.definition.ts index 392a7ef8321..02f7b5bf21a 100644 --- a/integrations/sunco/integration.definition.ts +++ b/integrations/sunco/integration.definition.ts @@ -6,7 +6,7 @@ import typingIndicator from 'bp_modules/typing-indicator' export default new IntegrationDefinition({ name: 'sunco', - version: '1.0.3', + version: '1.0.4', title: 'Sunshine Conversations', description: 'Give your bot access to a powerful omnichannel messaging platform.', icon: 'icon.svg', @@ -23,7 +23,7 @@ export default new IntegrationDefinition({ channel: { title: 'Sunshine Conversations Channel', description: 'Channel for a Sunshine conversation', - messages: { ...messages.defaults, markdown: messages.markdown }, + messages: { ...messages.defaults, markdown: messages.markdown, bloc: messages.markdownBloc }, message: { tags: { id: { diff --git a/integrations/teams/src/definitions/actions.ts b/integrations/teams/definitions/actions.ts similarity index 100% rename from integrations/teams/src/definitions/actions.ts rename to integrations/teams/definitions/actions.ts diff --git a/integrations/teams/definitions/channels.ts b/integrations/teams/definitions/channels.ts new file mode 100644 index 00000000000..b687d5e3cc5 --- /dev/null +++ b/integrations/teams/definitions/channels.ts @@ -0,0 +1,25 @@ +import { IntegrationDefinitionProps, messages } from '@botpress/sdk' + +export const channels = { + channel: { + title: 'Channel', + description: 'Teams conversation channel', + messages: messages.defaults, + message: { + tags: { + id: { + title: 'ID', + description: 'Teams activity ID', + }, + }, + }, + conversation: { + tags: { + id: { + title: 'ID', + description: 'Teams conversation ID', + }, + }, + }, + }, +} satisfies IntegrationDefinitionProps['channels'] diff --git a/integrations/teams/definitions/index.ts b/integrations/teams/definitions/index.ts new file mode 100644 index 00000000000..e35d76dfbd2 --- /dev/null +++ b/integrations/teams/definitions/index.ts @@ -0,0 +1,4 @@ +export { states } from './states' +export { actions } from './actions' +export { channels } from './channels' +export { user } from './user' diff --git a/integrations/teams/src/definitions/states.ts b/integrations/teams/definitions/states.ts similarity index 66% rename from integrations/teams/src/definitions/states.ts rename to integrations/teams/definitions/states.ts index e0ee8e40a14..1ddbfa51763 100644 --- a/integrations/teams/src/definitions/states.ts +++ b/integrations/teams/definitions/states.ts @@ -5,16 +5,16 @@ import type { ConversationReference } from 'botbuilder' type Is = A extends B ? (B extends A ? true : false) : false type Expect<_T extends true> = void -const channelAccount = z.object({ +export const channelAccountSchema = z.object({ id: z.string(), name: z.string(), }) -const convReference = z.object({ +const _convReferenceSchema = z.object({ activityId: z.string().optional().title('Activity ID').describe('The activity ID'), - user: channelAccount.optional().title('User').describe('User account of the user conversing with the bot'), + user: channelAccountSchema.optional().title('User').describe('User account of the user conversing with the bot'), locale: z.string().optional().title('Locale').describe('The locale of the conversation'), - bot: channelAccount.title('Bot').describe('Bot account of the bot'), + bot: channelAccountSchema.title('Bot').describe('Bot account of the bot'), conversation: z.any().title('Conversation').describe('Conversation reference'), // botbuilder typings are deceiving channelId: z.string().title('Channel ID').describe('The channel ID of the conversation'), serviceUrl: z @@ -22,18 +22,15 @@ const convReference = z.object({ .title('Service URL') .describe('Service endpoint where the operations concerning the conversation are performed'), }) - -const _convStateSchema = convReference.partial() -type ConvStateSchema = z.infer +const _convReferenceStateSchema = _convReferenceSchema.partial() +type ConvReferenceState = z.infer // this builds only if the state schema is the same type as ConversationReference from 'botbuilder' -type A = ConvStateSchema -type B = Partial -type _Test = Expect> +type _Test = Expect>> export const states = { conversation: { type: 'conversation', - schema: convReference.partial(), + schema: _convReferenceStateSchema, }, } satisfies IntegrationDefinitionProps['states'] diff --git a/integrations/teams/definitions/user.ts b/integrations/teams/definitions/user.ts new file mode 100644 index 00000000000..07a76ae4778 --- /dev/null +++ b/integrations/teams/definitions/user.ts @@ -0,0 +1,14 @@ +import { IntegrationDefinitionProps } from '@botpress/sdk' + +export const user = { + tags: { + id: { + title: 'ID', + description: 'Teams user ID', + }, + email: { + title: 'Email', + description: 'Email address', + }, + }, +} satisfies IntegrationDefinitionProps['user'] diff --git a/integrations/teams/hub.md b/integrations/teams/hub.md index a727b2dfe80..7a13e1b9194 100644 --- a/integrations/teams/hub.md +++ b/integrations/teams/hub.md @@ -1 +1,9 @@ The Microsoft Teams integration enables seamless collaboration between your AI-powered chatbot and Microsoft Teams, a popular workplace communication and collaboration platform. Connect your chatbot to Teams and enhance team productivity by automating tasks, providing instant support, and facilitating streamlined communication. With this integration, your chatbot can interact with users in Teams channels, respond to queries, deliver notifications, and perform actions within the Teams environment. Leverage Teams' robust features such as messaging, file sharing, meetings, and app integrations to create a powerful conversational AI experience. Boost teamwork and efficiency with the Microsoft Teams Integration for Botpress. + +## Migrating from version `1.x.x` to `2.x.x` + +Version `2.0.0` of the Microsoft Teams integration introduces changes to the channels (most notably the markdown channel). If you are migrating from version `1.x.x` to `2.x.x`, please note the following changes: + +- The "markdown" channel was removed in favor of integrating the behaviour into the "text" channel +- The "bloc" channel was implemented and can support up to 50 items per bloc message +- The "dropdown" channel was updated to display an actual dropdown instead of a selection of button options diff --git a/integrations/teams/integration.definition.ts b/integrations/teams/integration.definition.ts index ec5efbcc548..3c009e36c71 100644 --- a/integrations/teams/integration.definition.ts +++ b/integrations/teams/integration.definition.ts @@ -1,17 +1,22 @@ -import { IntegrationDefinition } from '@botpress/sdk' +import { IntegrationDefinition, z } from '@botpress/sdk' import { sentry as sentryHelpers } from '@botpress/sdk-addons' import typingIndicator from 'bp_modules/typing-indicator' - -import { actions, configuration, channels, user, states } from './src/definitions' +import { actions, channels, user, states } from 'definitions' export default new IntegrationDefinition({ name: 'teams', - version: '1.0.0', + version: '2.0.0', title: 'Microsoft Teams', description: 'Interact with users, deliver notifications, and perform actions within Microsoft Teams.', icon: 'icon.svg', readme: 'hub.md', - configuration, + configuration: { + schema: z.object({ + appId: z.string().min(1).title('App ID').describe('Teams application ID'), + appPassword: z.string().min(1).title('App Password').describe('Teams application password'), + tenantId: z.string().optional().title('Tenant ID').describe('Teams tenant ID'), + }), + }, channels, user, actions, diff --git a/integrations/teams/package.json b/integrations/teams/package.json index 3f9610308d1..ce8db16a5fa 100644 --- a/integrations/teams/package.json +++ b/integrations/teams/package.json @@ -15,16 +15,24 @@ "@botpress/sdk-addons": "workspace:*", "@microsoft/microsoft-graph-client": "^3.0.7", "axios": "^1.5.1", - "botbuilder": "^4.18.0", + "botbuilder": "^4.23.3", + "dedent": "^1.6.0", "jsonwebtoken": "^9.0.2", - "jwks-rsa": "^3.1.0" + "jwks-rsa": "^3.1.0", + "remark": "^15.0.1", + "remark-gfm": "^4.0.1", + "sanitize-html": "^2.17.0", + "turndown": "^7.2.2" }, "devDependencies": { "@botpress/cli": "workspace:*", "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "@sentry/cli": "^2.39.1", - "@types/jsonwebtoken": "^9.0.3" + "@types/jsonwebtoken": "^9.0.3", + "@types/mdast": "^4.0.4", + "@types/sanitize-html": "^2.16.0", + "@types/turndown": "^5.0.6" }, "bpDependencies": { "typing-indicator": "../../interfaces/typing-indicator" diff --git a/integrations/teams/src/actions/start-dm.ts b/integrations/teams/src/actions/start-dm.ts index 4b7ab026a1c..dca239f4e90 100644 --- a/integrations/teams/src/actions/start-dm.ts +++ b/integrations/teams/src/actions/start-dm.ts @@ -1,6 +1,6 @@ import { RuntimeError } from '@botpress/client' import { ConversationParameters, ConversationReference, TeamsChannelAccount, TeamsInfo, TurnContext } from 'botbuilder' -import { getUserByEmail } from '../client' +import { MicrosoftClient } from '../microsoft-api' import { getAdapter, getError } from '../utils' import * as bp from '.botpress' @@ -43,7 +43,7 @@ export const startDmConversation: bp.IntegrationProps['actions']['startDmConvers // ── If we only have an email, call TeamsInfo.getMember to fetch the account ── if (!teamsUserId?.length && teamsUserEmail?.length) { try { - const teamsUser = await getUserByEmail(ctx, teamsUserEmail) + const teamsUser = await MicrosoftClient.create(ctx).getUserByEmail(teamsUserEmail) teamsUserId = teamsUser?.id } catch (thrown) { const err = getError(thrown) diff --git a/integrations/teams/src/actions/typing-indicator.ts b/integrations/teams/src/actions/typing-indicator.ts index 9ed14de8037..426ed66fa58 100644 --- a/integrations/teams/src/actions/typing-indicator.ts +++ b/integrations/teams/src/actions/typing-indicator.ts @@ -1,5 +1,5 @@ import { ActivityTypes } from 'botbuilder' -import { getAdapter, getConversationReference } from 'src/utils' +import { getAdapter, getConversationReference } from '../utils' import * as bp from '.botpress' const DEFAULT_TIMEOUT = 5000 diff --git a/integrations/teams/src/channels.ts b/integrations/teams/src/channels.ts deleted file mode 100644 index c1f0f8b6a5a..00000000000 --- a/integrations/teams/src/channels.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { RuntimeError } from '@botpress/client' -import { - Activity, - ConversationReference, - CardFactory, - CardAction, - ActionTypes, - Attachment, - MessageFactory, -} from 'botbuilder' -import { getAdapter } from './utils' -import * as bp from '.botpress' - -type Choice = bp.channels.channel.choice.Choice -type Alternative = Choice['options'][number] - -type Card = bp.channels.channel.card.Card -type Action = Card['actions'][number] -type ActionType = Action['action'] - -const renderTeams = async ({ ctx, ack, conversation, client }: bp.AnyMessageProps, activity: Partial) => { - const { configuration } = ctx - const adapter = getAdapter(configuration) - - const stateRes = await client.getState({ - id: conversation.id, - name: 'conversation', - type: 'conversation', - }) - const { state } = stateRes - const convRef = state.payload as ConversationReference - - await adapter.continueConversation(convRef, async (turnContext) => { - if (!turnContext.activity.id) { - console.warn('No activity id found') - return - } - - await turnContext.sendActivity(activity) - await ack({ tags: { id: turnContext.activity.id } }) - }) -} - -const mapActionType = (action: ActionType): ActionTypes => { - if (action === 'postback') { - return ActionTypes.MessageBack - } - if (action === 'say') { - return ActionTypes.MessageBack - } - if (action === 'url') { - return ActionTypes.OpenUrl - } - return ActionTypes.MessageBack -} - -const mapAction = (action: Action): CardAction => ({ - type: mapActionType(action.action), - title: action.label, - value: action.value, - text: action.label, - displayText: action.label, -}) - -const mapChoice = (choice: Alternative): CardAction => ({ - type: ActionTypes.MessageBack, - title: choice.label, - displayText: choice.label, - value: choice.value, - text: choice.label, -}) - -const makeCard = (card: Card): Attachment => { - const { actions, imageUrl, subtitle, title } = card - const buttons: CardAction[] = actions.map(mapAction) - const images = imageUrl ? [{ url: imageUrl }] : [] - return CardFactory.heroCard(title, images, buttons, { subtitle }) -} - -const channel = { - messages: { - text: async (props) => { - const activity: Partial = { type: 'message', text: props.payload.text } - await renderTeams(props, activity) - }, - image: async (props) => { - const { imageUrl } = props.payload - const activity: Partial = { - type: 'message', - attachments: [CardFactory.heroCard('', [{ url: imageUrl }])], - } - await renderTeams(props, activity) - }, - markdown: async (props) => { - const { markdown } = props.payload - const activity: Partial = { - type: 'message', - attachments: [ - CardFactory.adaptiveCard({ - body: [ - { - type: 'TextBlock', - text: markdown, - }, // documentation here https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features - ], - }), - ], - } - await renderTeams(props, activity) - }, - audio: async (props) => { - const { audioUrl } = props.payload - const activity: Partial = { - type: 'message', - attachments: [CardFactory.audioCard('', [{ url: audioUrl }])], - } - await renderTeams(props, activity) - }, - video: async (props) => { - const { videoUrl } = props.payload - const activity: Partial = { - type: 'message', - attachments: [CardFactory.videoCard('', [{ url: videoUrl }])], - } - await renderTeams(props, activity) - }, - file: async (props) => { - const { fileUrl } = props.payload - const activity: Partial = { type: 'message', text: fileUrl } - await renderTeams(props, activity) - }, - location: async (props) => { - const { latitude, longitude } = props.payload - const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}` - const activity: Partial = { type: 'message', text: googleMapsLink } - await renderTeams(props, activity) - }, - carousel: async (props) => { - const { items } = props.payload - const activity = MessageFactory.carousel(items.map(makeCard)) - await renderTeams(props, activity) - }, - card: async (props) => { - const activity: Partial = { - type: 'message', - attachments: [makeCard(props.payload)], - } - await renderTeams(props, activity) - }, - choice: async (props) => { - const { options, text } = props.payload - const buttons: CardAction[] = options.map(mapChoice) - const activity: Partial = { - type: 'message', - attachments: [CardFactory.heroCard(text, [], buttons)], - } - await renderTeams(props, activity) - }, - dropdown: async (props) => { - // TODO: actually implement a dropdown and not a choice - // requires: - // - a submit button text - // - a dropdown placeholder - // - patience to mess around with adaptive cards - - const { options, text } = props.payload - const buttons: CardAction[] = options.map(mapChoice) - const activity: Partial = { - type: 'message', - attachments: [CardFactory.heroCard(text, [], buttons)], - } - await renderTeams(props, activity) - }, - bloc: () => { - throw new RuntimeError('Not implemented') - }, - }, -} satisfies bp.IntegrationProps['channels']['channel'] - -export const channels = { - channel, -} satisfies bp.IntegrationProps['channels'] diff --git a/integrations/teams/src/channels/constants.ts b/integrations/teams/src/channels/constants.ts new file mode 100644 index 00000000000..11e6d028674 --- /dev/null +++ b/integrations/teams/src/channels/constants.ts @@ -0,0 +1,2 @@ +export const DROPDOWN_VALUE_KIND = 'dropdown_value' as const +export const DROPDOWN_VALUE_ID = 'choice' as const diff --git a/integrations/teams/src/handler.ts b/integrations/teams/src/channels/inbound.ts similarity index 57% rename from integrations/teams/src/handler.ts rename to integrations/teams/src/channels/inbound.ts index f01ace6304a..fb8559d9d19 100644 --- a/integrations/teams/src/handler.ts +++ b/integrations/teams/src/channels/inbound.ts @@ -1,23 +1,10 @@ import { Activity, ConversationReference, TurnContext, TeamsInfo, TeamsChannelAccount } from 'botbuilder' -import { authorizeRequest } from './signature' -import { getAdapter, sleep } from './utils' +import { transformTeamsHtmlToStdMarkdown } from '../markdown/teams-html-to-markdown' +import { getAdapter, sleep } from '../utils' +import { DROPDOWN_VALUE_ID, DROPDOWN_VALUE_KIND } from './constants' import * as bp from '.botpress' -export const handler: bp.IntegrationProps['handler'] = async ({ req, client, ctx, logger }) => { - await authorizeRequest(req) - - if (!req.body) { - console.warn('Handler received an empty body') - return - } - - const activity: Activity = JSON.parse(req.body) - console.info(`Handler received event of type ${activity.type}`) - - if (!activity.id) { - return - } - +export const processInboundChannelMessage = async ({ client, ctx, logger }: bp.HandlerProps, activity: Activity) => { const convRef: Partial = TurnContext.getConversationReference(activity) const senderChannelAccount = activity.from! @@ -61,15 +48,37 @@ export const handler: bp.IntegrationProps['handler'] = async ({ req, client, ctx }, }) + const message = _extractMessage(activity) await client.getOrCreateMessage({ tags: { id: activity.id }, type: 'text', userId: user.id, conversationId: conversation.id, - payload: { text: activity.text }, + payload: { text: message }, }) break default: return } } + +const _extractMessage = (activity: Activity): string => { + // Handle dropdown value + if (activity.value && activity.value.kind === DROPDOWN_VALUE_KIND) { + return String(activity.value[DROPDOWN_VALUE_ID]) + } + + // Handle HTML attachment (Any richtext/markdown will convert/show up as HTML) + if (activity.attachments) { + const htmlAttachment = activity.attachments.find((attachment) => attachment.contentType === 'text/html') + if (htmlAttachment && typeof htmlAttachment.content === 'string') { + return transformTeamsHtmlToStdMarkdown(htmlAttachment.content) + } + } + + /** Fallback to plain text + * + * @remark Using coalescence operator (??) since messages + * with no text can happen (e.g. image only messages) */ + return activity.text ?? '' +} diff --git a/integrations/teams/src/channels/outbound.ts b/integrations/teams/src/channels/outbound.ts new file mode 100644 index 00000000000..7c7452a5988 --- /dev/null +++ b/integrations/teams/src/channels/outbound.ts @@ -0,0 +1,259 @@ +import { RuntimeError } from '@botpress/client' +import { + Activity, + ConversationReference, + CardFactory, + CardAction, + ActionTypes, + Attachment, + MessageFactory, +} from 'botbuilder' +import { transformMarkdownToTeamsXml } from '../markdown/markdown-to-teams-xml' +import { getAdapter } from '../utils' +import { DROPDOWN_VALUE_ID, DROPDOWN_VALUE_KIND } from './constants' +import * as bp from '.botpress' + +type MessageHandlerProps = bp.MessageProps['channel'][T] + +type ChoicePayload = MessageHandlerProps<'choice'>['payload'] +type ChoiceOption = ChoicePayload['options'][number] +type DropdownPayload = MessageHandlerProps<'dropdown'>['payload'] +type DropdownOption = DropdownPayload['options'][number] + +type BotpressCard = bp.channels.channel.card.Card +type Action = BotpressCard['actions'][number] +type ActionType = Action['action'] + +// ====== Message Channel Helpers ====== + +const _renderTeams = async ( + { ctx, ack, conversation, client, logger }: bp.AnyMessageProps, + activity: Partial +) => { + const { configuration } = ctx + const adapter = getAdapter(configuration) + + const stateRes = await client.getState({ + id: conversation.id, + name: 'conversation', + type: 'conversation', + }) + const { state } = stateRes + const convRef = state.payload as ConversationReference + + await adapter.continueConversation(convRef, async (turnContext) => { + if (!turnContext.activity.id) { + logger.forBot().warn('No activity id found') + return + } + + await turnContext.sendActivity(activity) + await ack({ tags: { id: turnContext.activity.id } }) + }) +} + +const _mapActionType = (action: ActionType): ActionTypes => { + if (action === 'postback') { + return ActionTypes.MessageBack + } + if (action === 'say') { + return ActionTypes.MessageBack + } + if (action === 'url') { + return ActionTypes.OpenUrl + } + return ActionTypes.MessageBack +} + +const _mapAction = (action: Action): CardAction => ({ + type: _mapActionType(action.action), + title: action.label, + value: action.value, + text: action.label, + displayText: action.label, +}) + +const _mapChoice = (choice: ChoiceOption): CardAction => ({ + type: ActionTypes.MessageBack, + title: choice.label, + displayText: choice.label, + value: choice.value, + text: choice.label, +}) + +const _makeCard = (card: BotpressCard): Attachment => { + const { actions, imageUrl, subtitle, title } = card + const buttons: CardAction[] = actions.map(_mapAction) + const images = imageUrl ? [{ url: imageUrl }] : [] + return CardFactory.heroCard(title, images, buttons, { subtitle }) +} + +const _makeDropdownCard = (text: string, options: DropdownOption[]): Attachment => { + const choices = options.map((option: DropdownOption) => ({ + title: option.label, + value: option.value, + })) + + return CardFactory.adaptiveCard({ + // documentation here https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features + body: [ + { + type: 'TextBlock', + text, + wrap: true, + weight: 'Bolder', + }, + { + id: DROPDOWN_VALUE_ID, + type: 'Input.ChoiceSet', + placeholder: 'Select...', + style: 'compact', + choices, + }, + ], + actions: [ + { + type: 'Action.Submit', + title: 'Submit', + data: { kind: DROPDOWN_VALUE_KIND }, + }, + ], + }) +} + +// ====== Message Channel Handlers ====== + +const _handleTextMessage = async (props: MessageHandlerProps<'text'>) => { + const { text } = props.payload + const xml = transformMarkdownToTeamsXml(text) + const activity: Partial = { + type: 'message', + textFormat: 'xml', + text: xml, + } + await _renderTeams(props, activity) +} + +const _handleImageMessage = async (props: MessageHandlerProps<'image'>) => { + const { imageUrl } = props.payload + const activity: Partial = { + type: 'message', + attachments: [CardFactory.heroCard('', [{ url: imageUrl }])], + } + await _renderTeams(props, activity) +} + +const _handleAudioMessage = async (props: MessageHandlerProps<'audio'>) => { + const { audioUrl } = props.payload + const activity: Partial = { + type: 'message', + attachments: [CardFactory.audioCard('', [{ url: audioUrl }])], + } + await _renderTeams(props, activity) +} + +const _handleVideoMessage = async (props: MessageHandlerProps<'video'>) => { + const { videoUrl } = props.payload + const activity: Partial = { + type: 'message', + attachments: [CardFactory.videoCard('', [{ url: videoUrl }])], + } + await _renderTeams(props, activity) +} + +const _handleFileMessage = async (props: MessageHandlerProps<'file'>) => { + const { fileUrl } = props.payload + const activity: Partial = { type: 'message', text: fileUrl } + await _renderTeams(props, activity) +} + +const _handleLocationMessage = async (props: MessageHandlerProps<'location'>) => { + const { latitude, longitude } = props.payload + const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}` + const activity: Partial = { type: 'message', text: googleMapsLink } + await _renderTeams(props, activity) +} + +const _handleCarouselMessage = async (props: MessageHandlerProps<'carousel'>) => { + const { items } = props.payload + const activity = MessageFactory.carousel(items.map(_makeCard)) + await _renderTeams(props, activity) +} + +const _handleCardMessage = async (props: MessageHandlerProps<'card'>) => { + const activity: Partial = { + type: 'message', + attachments: [_makeCard(props.payload)], + } + await _renderTeams(props, activity) +} + +const _handleChoiceMessage = async (props: MessageHandlerProps<'choice'>) => { + const { options, text } = props.payload + const buttons: CardAction[] = options.map(_mapChoice) + const activity: Partial = { + type: 'message', + attachments: [CardFactory.heroCard(text, [], buttons)], + } + await _renderTeams(props, activity) +} + +const _handleDropdownMessage = async (props: MessageHandlerProps<'dropdown'>) => { + const { options, text } = props.payload + const activity: Partial = { + type: 'message', + attachments: [_makeDropdownCard(text, options)], + } + await _renderTeams(props, activity) +} + +const _handleBlocMessage = async ({ payload, ...rest }: MessageHandlerProps<'bloc'>) => { + if (payload.items.length > 50) { + throw new RuntimeError('Teams only allows 50 messages to be sent every 1 second(s)') + } + + for (const item of payload.items) { + const messageProps = { ...rest, ...item } + switch (messageProps.type) { + case 'text': + await _handleTextMessage(messageProps) + continue + case 'image': + await _handleImageMessage(messageProps) + continue + case 'audio': + await _handleAudioMessage(messageProps) + continue + case 'video': + await _handleVideoMessage(messageProps) + continue + case 'file': + await _handleFileMessage(messageProps) + continue + case 'location': + await _handleLocationMessage(messageProps) + continue + default: + messageProps satisfies never + throw new RuntimeError(`Unsupported message type: ${(messageProps as any)?.type ?? 'Unknown'}`) + } + } +} + +export const channels = { + channel: { + messages: { + text: _handleTextMessage, + image: _handleImageMessage, + audio: _handleAudioMessage, + video: _handleVideoMessage, + file: _handleFileMessage, + location: _handleLocationMessage, + carousel: _handleCarouselMessage, + card: _handleCardMessage, + choice: _handleChoiceMessage, + dropdown: _handleDropdownMessage, + bloc: _handleBlocMessage, + }, + }, +} satisfies bp.IntegrationProps['channels'] diff --git a/integrations/teams/src/client.ts b/integrations/teams/src/client.ts deleted file mode 100644 index 322f5f54362..00000000000 --- a/integrations/teams/src/client.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ClientSecretCredential } from '@azure/identity' -import { Client } from '@microsoft/microsoft-graph-client' -import * as bp from '../.botpress' - -function getGraphClient({ ctx }: { ctx: bp.Context }) { - const { tenantId, appId, appPassword } = ctx.configuration - - const credential = new ClientSecretCredential(tenantId as string, appId, appPassword) - - return Client.initWithMiddleware({ - authProvider: { - getAccessToken: async () => { - const token = await credential.getToken('https://graph.microsoft.com/.default') - return token?.token! - }, - }, - }) -} - -export async function getUserByEmail(ctx: bp.Context, email: string) { - try { - const graphClient = getGraphClient({ ctx }) - return await graphClient.api(`/users/${email}`).get() - } catch (err: any) { - if (err.statusCode === 404) { - throw new Error(`No user found with email: ${email}`) - } - throw err - } -} diff --git a/integrations/teams/src/definitions/index.ts b/integrations/teams/src/definitions/index.ts deleted file mode 100644 index 67adece0377..00000000000 --- a/integrations/teams/src/definitions/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { z, IntegrationDefinitionProps, messages } from '@botpress/sdk' - -export { states } from './states' -export { actions } from './actions' - -export const configuration = { - schema: z.object({ - appId: z.string().min(1).title('App ID').describe('Teams application ID'), - appPassword: z.string().min(1).title('App Password').describe('Teams application password'), - tenantId: z.string().optional().title('Tenant ID').describe('Teams tenant ID'), - }), -} satisfies IntegrationDefinitionProps['configuration'] - -export const channels = { - channel: { - title: 'Channel', - description: 'Teams conversation channel', - messages: { ...messages.defaults, markdown: messages.markdown }, - message: { - tags: { - id: { - title: 'ID', - description: 'Teams activity ID', - }, - }, - }, - conversation: { - tags: { - id: { - title: 'ID', - description: 'Teams conversation ID', - }, - }, - }, - }, -} satisfies IntegrationDefinitionProps['channels'] - -export const user = { - tags: { - id: { - title: 'ID', - description: 'Teams user ID', - }, - email: { - title: 'Email', - description: 'Email address', - }, - }, -} satisfies IntegrationDefinitionProps['user'] diff --git a/integrations/teams/src/index.ts b/integrations/teams/src/index.ts index 6cc76f9650f..df1b3f467e5 100644 --- a/integrations/teams/src/index.ts +++ b/integrations/teams/src/index.ts @@ -1,30 +1,13 @@ -import { RuntimeError } from '@botpress/sdk' import { sentry as sentryHelpers } from '@botpress/sdk-addons' - -import axios, { isAxiosError } from 'axios' import actions from './actions' -import { channels } from './channels' -import { handler } from './handler' +import { channels } from './channels/outbound' +import { register, unregister } from './setup' +import { handler } from './webhooks/handler' import * as bp from '.botpress' const integration = new bp.Integration({ - register: async ({ ctx }) => { - const tenant = ctx.configuration.tenantId ?? 'botframework.com' - - const params = new URLSearchParams({ - grant_type: 'client_credentials', - client_id: ctx.configuration.appId, - client_secret: ctx.configuration.appPassword, - tenant_id: tenant, - scope: 'https://api.botframework.com/.default', - }) - - await axios.post(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, params.toString()).catch((e) => { - const message = isAxiosError(e) ? e.response?.data?.error_description : e.message - throw new RuntimeError(`Failed to authenticate with Microsoft Teams: ${message}`) - }) - }, - unregister: async () => {}, + register, + unregister, actions, channels, handler, diff --git a/integrations/teams/src/markdown/markdown-to-teams-xml.test.ts b/integrations/teams/src/markdown/markdown-to-teams-xml.test.ts new file mode 100644 index 00000000000..388e4219734 --- /dev/null +++ b/integrations/teams/src/markdown/markdown-to-teams-xml.test.ts @@ -0,0 +1,348 @@ +import { describe, expect, test } from 'vitest' +import type { TestCase } from '../types' +import { transformMarkdownToTeamsXml } from './markdown-to-teams-xml' +import dedent from 'dedent' + +type MarkdownToTeamsHtmlTestCase = TestCase + +const markdownToTeamsHtmlTestCases: MarkdownToTeamsHtmlTestCase[] = [ + // ----- Bold ----- + { + input: '**Bold**', + expects: 'Bold', + description: 'Apply bold style to text', + }, + { + input: '__Bold__', + expects: 'Bold', + description: 'Alternative apply bold style to text', + }, + // ----- Italic ----- + { + input: '*Italic*', + expects: 'Italic', + description: 'Apply italic style to text', + }, + { + input: '_Italic_', + expects: 'Italic', + description: 'Alternative apply italic style to text', + }, + // ----- Strikethrough ----- + { + input: '~~Strike~~', + expects: 'Strike', + description: 'Apply strikethrough style to text', + }, + // ----- Code Snippet ----- + { + input: '`Code Snippet`', + expects: 'Code Snippet', + description: 'Apply code style to text', + }, + // ----- Code Blocks ----- + { + input: '```\nconsole.log("Code Block")\n```', + expects: '
console.log("Code Block")\n
', + description: 'Apply code block style to text - Without language', + }, + { + input: '```typescript\nconsole.log("Code Block")\n```', + expects: '
console.log("Code Block")\n
', + description: 'Apply code block style to text - With language', + }, + { + input: '\tconsole.log("Indented Code Block")', + expects: '
console.log("Indented Code Block")\n
', + description: 'Apply alternative code block style to text using indentation', + }, + // ----- Blockquote ----- + { + input: '> Blockquote', + expects: '
\n\nBlockquote\n
', + description: 'Apply blockquote style to text', + }, + // ----- Headers ----- + { + input: '# Header 1', + expects: '

Header 1

', + description: 'Apply h1 header style to text', + }, + { + input: '## Header 2', + expects: '

Header 2

', + description: 'Apply h2 header style to text', + }, + { + input: '### Header 3', + expects: '

Header 3

', + description: 'Apply h3 header style to text', + }, + { + input: 'Header 1\n===\nHello World', + expects: '

Header 1

\nHello World', + description: 'Apply alternate h1 header style to text', + }, + { + input: 'Header 2\n---\nHello World', + expects: '

Header 2

\nHello World', + description: 'Apply alternate h2 header style to text', + }, + { + input: '#### Header 4', + expects: 'Header 4', + description: "Don't apply h4 header style to text since Teams doesn't support headers larger than h3", + }, + { + input: '##### Header 5', + expects: 'Header 5', + description: "Don't apply h5 header style to text since Teams doesn't support headers larger than h3", + }, + { + input: '###### Header 6', + expects: 'Header 6', + description: "Don't apply h6 header style to text since Teams doesn't support headers larger than h3", + }, + // ----- Horizontal Rule ----- + // Note: For e2e tests, text is required before the horizontal rule, otherwise nothing is rendered + { + input: 'Hello World\n\n---', + expects: 'Hello World\n
', + description: 'Insert horizontal rule using dash notation', + }, + { + input: 'Hello World\n***', + expects: 'Hello World\n
', + description: 'Insert horizontal rule using asterisk notation', + }, + { + input: 'Hello World\n___', + expects: 'Hello World\n
', + description: 'Insert horizontal rule using underscore notation', + }, + // ----- Hyperlinks ----- + { + input: '[Hyperlink](https://www.botpress.com/)', + expects: 'Hyperlink', + description: 'Convert hyperlink markup to html link', + }, + { + input: '[Hyperlink](https://www.botpress.com/ "Tooltip Title")', + expects: 'Hyperlink', + // NOTE: Teams does not support the title attribute, however, it just ignores it instead of causing a crash + description: 'Markdown hyperlink title gets carried over to html link', + }, + { + input: '[Hyperlink][id]\n\n[id]: https://www.botpress.com/ "Tooltip Title"', + expects: 'Hyperlink', + // NOTE: Teams does not support the title attribute, however, it just ignores it instead of causing a crash + description: 'Convert hyperlink markup using footnote style syntax to html link', + }, + { + input: 'https://www.botpress.com/', + expects: 'https://www.botpress.com/', + description: 'Implicit link gets auto-converted into html link', + }, + // ----- Telephone Hyperlinks ----- + { + input: '[Phone Number](tel:5141234567)', + expects: 'Phone Number', + description: 'Strip phone number markdown to plain text phone number since Teams does not support "tel" links', + }, + { + input: '[Phone Number](tel:5141234567 "Tooltip Title")', + expects: 'Phone Number', + description: 'Strip phone number markdown with title attribute, since Teams does not support "tel" links', + }, + { + input: '[Phone Number][id]\n\n[id]: tel:5141234567 "Tooltip Title"', + expects: 'Phone Number', + description: 'Strip phone number markdown using footnote style syntax since Teams does not support "tel" links', + }, + // ----- Email Hyperlinks ----- + { + input: '[Botpress Email](mailto:test@botpress.com)', + expects: 'Botpress Email', + description: 'Convert email link markdown to mailto email link', + }, + { + input: '[Botpress Email](mailto:test@botpress.com "Tooltip Title")', + expects: 'Botpress Email', + description: 'Convert email link markdown with title attribute to mailto email link', + }, + { + input: '[Botpress Email][id]\n\n[id]: mailto:test@botpress.com "Tooltip Title"', + expects: 'Botpress Email', + description: 'Convert email link markdown using footnote style syntax to mailto email link', + }, + // ----- Images ----- + { + input: '![Botpress Brand Logo](https://shop.botpress.com/cdn/shop/files/logo.png?v=1708026010&width=600)', + expects: + 'Botpress Brand Logo', + description: 'Markdown image is converted to html image with alt text attribute', + }, + { + input: + '![Botpress Brand Logo](https://shop.botpress.com/cdn/shop/files/logo.png?v=1708026010&width=600 "Title Tooltip Text")', + expects: + 'Botpress Brand Logo', + description: 'Markdown image is converted to html image with alt text & title attributes', + }, + // ----- Lists ----- + { + input: '- Item 1\n- Item 2\n- Item 3', + expects: '
    \n
  • \nItem 1\n
  • \n
  • \nItem 2\n
  • \n
  • \nItem 3\n
  • \n
', + description: 'Markdown unordered lists convert to html unordered lists', + }, + { + input: '1) Item 1\n2) Item 2\n3) Item 3', + expects: '
    \n
  1. \nItem 1\n
  2. \n
  3. \nItem 2\n
  4. \n
  5. \nItem 3\n
  6. \n
', + description: 'Markdown ordered lists convert to html ordered lists', + }, + { + input: '- [ ] Foo\n- [x] Bar', + expects: '
    \n
  • \n☐ Foo\n
  • \n
  • \n☑\uFE0E Bar\n
  • \n
', + description: 'Dashed markdown task lists are converted to lists with checkbox unicode characters', + }, + { + input: '1. [ ] Foo\n2. [x] Bar', + expects: '
    \n
  1. \n☐ Foo\n
  2. \n
  3. \n☑\uFE0E Bar\n
  4. \n
', + description: 'Numbered markdown task lists are converted to lists with checkbox unicode characters', + }, + // ----- Tables ----- + { + input: '| Item 1 | Item 2 | Item 3 |\n| - | - | - |\n| Value 1 | Value 2 | Value 3 |', + expects: dedent` + + + + + + + + + + + +
Item 1Item 2Item 3
Value 1Value 2Value 3
`, + description: 'Markdown tables convert to html tables', + }, + // ==== Advanced Tests & Edge Cases ==== + { + input: '- Item 1\n\t- Item A\n\t- Item B\n- Item 2\n- Item 3', + expects: + '
    \n
  • \nItem 1
      \n
    • \nItem A\n
    • \n
    • \nItem B\n
    • \n
    \n\n
  • \n
  • \nItem 2\n
  • \n
  • \nItem 3\n
  • \n
', + description: 'Nested markdown unordered lists convert to nested html unordered lists', + }, + { + input: '1) Item 1\n\t1) Item A\n\t2) Item B\n2) Item 2\n3) Item 3', + expects: + '
    \n
  1. \nItem 1
      \n
    1. \nItem A\n
    2. \n
    3. \nItem B\n
    4. \n
    \n\n
  2. \n
  3. \nItem 2\n
  4. \n
  5. \nItem 3\n
  6. \n
', + description: 'Nested markdown ordered lists convert to nested html ordered lists', + }, + { + input: '1) Ordered Item 1\n\t- Unordered Item A\n\t- Unordered Item B\n2) Ordered Item 2\n3) Ordered Item 3', + expects: + '
    \n
  1. \nOrdered Item 1
      \n
    • \nUnordered Item A\n
    • \n
    • \nUnordered Item B\n
    • \n
    \n\n
  2. \n
  3. \nOrdered Item 2\n
  4. \n
  5. \nOrdered Item 3\n
  6. \n
', + description: + 'Unordered markdown (md) list nested in ordered md list convert to unordered html list nested in ordered html list', + }, + { + input: '- Unordered Item 1\n\t1) Ordered Item A\n\t2) Ordered Item B\n- Unordered Item 2\n- Unordered Item 3', + expects: + '
    \n
  • \nUnordered Item 1
      \n
    1. \nOrdered Item A\n
    2. \n
    3. \nOrdered Item B\n
    4. \n
    \n\n
  • \n
  • \nUnordered Item 2\n
  • \n
  • \nUnordered Item 3\n
  • \n
', + description: + 'Ordered markdown (md) list nested in unordered md list converts to ordered html list nested in unordered html list', + }, + { + input: '> Blockquote Layer 1\n> > Blockquote Layer 2\n> > > Blockquote Layer 3', + expects: + '
\n\nBlockquote Layer 1
\n\nBlockquote Layer 2
\n\nBlockquote Layer 3\n
\n
\n
', + // This is supported in Teams based on manual testing (Last tested: 2025-11-20) + description: 'Apply nested blockquotes to text', + }, + { + input: '(c) (C) (r) (R) (tm) (TM) +-', + expects: '© © ® ® ™ ™ ±', + description: 'Convert text into their typographic equivalents', + }, + { + input: 'Word -- ---', + expects: 'Word – —', + description: 'Convert 2 dashes into an "en dash" & 3 dashes into an "em dash" (Must follow a word)', + }, + { + input: 'Hello\n\n---\n\nBotpress\n***\nWorld\n___', + expects: 'Hello\n
\nBotpress\n
\nWorld\n
', + // NOTE: 3 dashes variant requires an additional newline, otherwise it converts into a size 2 header + description: 'Convert markdown thematic breaks (e.g. 3 dashes, asterisks, or underscores) into horizontal rules', + }, + { + input: '**~~Bold-Strike~~**', + expects: 'Bold-Strike', + description: 'Multiple nested effects all get applied', + }, + { + input: '`**Code-Bold**`', + expects: '**Code-Bold**', + description: 'Markdown nested within a code snippet does not get converted to HTML', + }, + { + input: '```\n**CodeBlock-Bold**\n```', + expects: '
**CodeBlock-Bold**\n
', + description: 'Markdown nested within a code block does not get converted to HTML', + }, + { + input: 'This is line one. \nThis is line two.', + expects: 'This is line one.
\nThis is line two.', + description: 'Converts hardbreak into
', + }, + { + input: '_cut**off_**', + expects: 'cut**off**', + description: 'Markdown that gets cutoff (bold in this case) by another markdown does not convert to html', + }, + { + input: '| Item 1 | Item 2 | Item 3 |\n| Value 1 | Value 2 | Value 3 |', + expects: '| Item 1 | Item 2 | Item 3 |\n| Value 1 | Value 2 | Value 3 |', + description: 'Markdown table without the splitter row does not convert to html', + }, + { + input: '', + expects: '<label><b>Hello World!</b><br /></label>', + description: 'HTML tags are escaped', + }, +] + +describe('Standard Markdown to Teams HTML Conversion', () => { + markdownToTeamsHtmlTestCases.forEach(({ input, expects, description, skip = false }: MarkdownToTeamsHtmlTestCase) => { + test.skipIf(skip)(description, () => { + const html = transformMarkdownToTeamsXml(input) + expect(html).toBe(expects) + }) + }) + + test('Ensure javascript injection via markdown link is not possible', () => { + const html = transformMarkdownToTeamsXml("[click me](javascript:alert('XSS'))") + expect(html).toBe('click me') + }) + + test('Ensure javascript injection via markdown link reference is not possible', () => { + const html = transformMarkdownToTeamsXml( + '[click me][id]\n\n[id]: javascript:alert(\'LinkReferenceXSS\') "Tooltip Title"' + ) + expect(html).toBe('click me') + }) + + test('Ensure javascript injection via html anchor tag is not possible', () => { + const html = transformMarkdownToTeamsXml('click me') + expect(html).toBe('<a href="javascript:alert(\'XSS\')">click me</a>') + }) + + test('Ensure javascript injection via html image tag is not possible', () => { + const html = transformMarkdownToTeamsXml('alt text') + + expect(html).toBe('<img src="image.jpg" alt="alt text" onerror="alert(\'xss\')">') + }) +}) diff --git a/integrations/teams/src/markdown/markdown-to-teams-xml.ts b/integrations/teams/src/markdown/markdown-to-teams-xml.ts new file mode 100644 index 00000000000..928bdbf25c4 --- /dev/null +++ b/integrations/teams/src/markdown/markdown-to-teams-xml.ts @@ -0,0 +1,217 @@ +import type { Parent } from 'mdast' +import { remark } from 'remark' +import remarkGfm from 'remark-gfm' +import { escapeAndSanitizeHtml, isNaughtyUrl, sanitizeHtml } from './sanitize-utils' +import type { + DefinedLinkReference, + DefinitionNodeData, + MarkdownHandlers, + NodeHandler, + TableCellWithHeaderInfo, +} from './types' + +export const defaultHandlers: MarkdownHandlers = { + blockquote: (node, visit) => `
\n\n${visit(node)}\n
`, + break: () => '
\n', + code: (node) => `${node.value}\n`, + delete: (node, visit) => `${visit(node)}`, + emphasis: (node, visit) => `${visit(node)}`, + footnoteDefinition: (node, visit) => `[${node.identifier}] ${visit(node)}\n`, + footnoteReference: (node) => `[${node.identifier}]`, + heading: (node, visit) => { + // This approach of using font-size with bold is more + // consistent cross-platform than using

,

,

. + // Also, the h1-h3 don't render correctly on Teams Desktop app. + const nodeContent = visit(node) + switch (node.depth) { + case 1: + return `

${nodeContent}

\n` + case 2: + return `

${nodeContent}

\n` + case 3: + return `

${nodeContent}

\n` + default: + return `${nodeContent}\n` + } + }, + html: (node) => escapeAndSanitizeHtml(node.value), + image: (node) => _createSanitizedImage(node), + inlineCode: (node) => `${node.value}`, + link: (node, visit) => _createSanitizedHyperlink(node, visit(node)), + linkReference: (node, visit) => { + const { linkDefinition } = node + const nodeContent = visit(node) + if (!linkDefinition) { + return nodeContent + } + return _createSanitizedHyperlink(linkDefinition, nodeContent) + }, + list: (node, visit) => { + const tag = node.ordered ? 'ol' : 'ul' + return `<${tag}>\n${visit(node)}\n` + }, + listItem: (node, visit) => { + let checkbox = '' + const isChecked = node.checked ?? null + if (isChecked !== null) { + checkbox = isChecked ? '☑︎ ' : '☐ ' + } + + return `
  • \n${checkbox}${visit(node)}\n
  • \n` + }, + paragraph: (node, visit, parents) => `${visit(node)}${parents.at(-1)?.type === 'root' ? '\n' : ''}`, + strong: (node, visit) => `${visit(node)}`, + table: (node, visit) => { + const headerRow = node.children[0] + headerRow?.children.forEach((cell: TableCellWithHeaderInfo) => { + cell.isHeader = true + }) + + return `\n${visit(node)}
    ` + }, + tableRow: (node, visit) => `\n${visit(node)}\n`, + tableCell: (node, visit) => { + const tag = node.isHeader === true ? 'th' : 'td' + return `<${tag}>${visit(node)}\n` + }, + text: (node) => node.value, + thematicBreak: () => '
    \n', + definition: (_node) => '', +} + +type HyperlinkProps = { + url: string + title?: string | null +} + +const _createSanitizedHyperlink = (props: HyperlinkProps, content: string): string => { + const { url, title } = props + if (isNaughtyUrl('a', url)) { + return content + } + + return `${content}` +} + +type ImageProps = { + url: string + alt?: string | null + title?: string | null +} + +const _createSanitizedImage = (props: ImageProps): string => { + const { url, alt: altText, title } = props + const optionalAttrs = `${altText ? ` alt="${altText}"` : ''}${title ? ` title="${title}"` : ''}` + if (isNaughtyUrl('img', url)) { + return `` + } + + return `` +} + +const _isNodeType = (s: string, handlers: MarkdownHandlers): s is keyof MarkdownHandlers => s in handlers + +const _extractDefinitions = (parentNode: Parent): Record => { + let definitions: Record = {} + for (const node of parentNode.children) { + if ('children' in node) { + const extractedDefs = _extractDefinitions(node) + + definitions = { ...definitions, ...extractedDefs } + } + + if (node.type === 'definition') { + definitions[node.identifier] = { + identifier: node.identifier, + label: node.label, + url: node.url, + title: node.title, + } + } + } + + return definitions +} + +const _visitTree = ( + tree: Parent, + handlers: MarkdownHandlers, + parents: Parent[], + definitions: Record +): string => { + let tmp = '' + let footnoteTmp = '' + parents.push(tree) + + for (const node of tree.children) { + if (!_isNodeType(node.type, handlers)) { + throw new Error(`The Markdown node type [${node.type}] is not supported`) + } + + const handler = handlers[node.type] as NodeHandler + + if (node.type === 'linkReference') { + const linkReferenceNode = node as DefinedLinkReference + const def = definitions[node.identifier] + linkReferenceNode.linkDefinition = def + + tmp += handler( + linkReferenceNode, + (n) => _visitTree(n, handlers, parents, definitions), + parents, + handlers, + definitions + ) + continue + } + + if (node.type === 'footnoteDefinition') { + footnoteTmp += handler(node, (n) => _visitTree(n, handlers, parents, definitions), parents, handlers, definitions) + continue + } + tmp += handler(node, (n) => _visitTree(n, handlers, parents, definitions), parents, handlers, definitions) + } + parents.pop() + return `${tmp}${footnoteTmp}` +} + +export const transformMarkdownToTeamsXml = (markdown: string, handlers: MarkdownHandlers = defaultHandlers): string => { + const tree = remark().use(remarkGfm).parse(markdown) + const definitions = _extractDefinitions(tree) + let html = _visitTree(tree, handlers, [], definitions).trim() + _replacers.forEach((replacer) => { + html = replacer(html) + }) + return sanitizeHtml(html) +} + +// ==== Replacer functions ==== +function ellipses(input: string): string { + return input.replace(/\.{3}/gim, '…') +} + +function copyright(input: string): string { + return input.replace(/\(c\)/gi, '©') +} + +function registeredTrademark(input: string): string { + return input.replace(/\(r\)/gi, '®') +} + +function trademark(input: string): string { + return input.replace(/\(tm\)/gi, '™') +} + +function plusOrMinus(input: string): string { + return input.replace(/\+-/gi, '±') +} + +function enDash(input: string): string { + return input.replace(/(?) => + SanitizeHTML(html, options ? Object.assign({}, defaultSanitizeConfig, options) : defaultSanitizeConfig) + +/** Avoid false positives with .__proto__, .hasOwnProperty, etc. + * + * @remark Pulled from the "sanitize-html" package */ +function has(obj: unknown, key: string) { + return {}.hasOwnProperty.call(obj, key) +} + +/** Checks if the provided URL is "naughty" (AKA potentially malicious). + * + * @remark Pulled from the "sanitize-html" package to handle URL scanning + * for XSS with some minor adjustments (Mainly to make TypeScript happy) + * @remark The original function name in the package is "naughtyHref" */ +export function isNaughtyUrl(tagName: string, href: string) { + // Browsers ignore character codes of 32 (space) and below in a surprising + // number of situations. Start reading here: + // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_tab + + href = href.replace(/[\x00-\x20]+/g, '') + // Clobber any comments in URLs, which the browser might + // interpret inside an XML data island, allowing + // a javascript: URL to be snuck through + while (true) { + const firstIndex = href.indexOf('', firstIndex + 4) + if (lastIndex === -1) { + break + } + href = href.substring(0, firstIndex) + href.substring(lastIndex + 3) + } + // Case insensitive so we don't get faked out by JAVASCRIPT #1 + // Allow more characters after the first so we don't get faked + // out by certain schemes browsers accept + const matches = href.match(/^([a-zA-Z][a-zA-Z0-9.\-+]*):/) + const scheme = matches?.[1]?.toLowerCase() + if (!scheme) { + // Protocol-relative URL starting with any combination of '/' and '\' + if (href.match(/^[/\\]{2}/)) { + return !defaultSanitizeConfig.allowProtocolRelative + } + + // No scheme + return false + } + + if (has(defaultSanitizeConfig.allowedSchemesByTag, tagName)) { + return defaultSanitizeConfig.allowedSchemesByTag[tagName]?.indexOf(scheme) === -1 + } + + return !defaultSanitizeConfig.allowedSchemes || defaultSanitizeConfig.allowedSchemes.indexOf(scheme) === -1 +} + +export const escapeAndSanitizeHtml = (html: string) => { + const escapedHtml = html + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') + + // And sanitize it for good measure in + // case above replacers missed something + return sanitizeHtml(escapedHtml) +} diff --git a/integrations/teams/src/markdown/teams-html-to-markdown.test.ts b/integrations/teams/src/markdown/teams-html-to-markdown.test.ts new file mode 100644 index 00000000000..2098fb3c524 --- /dev/null +++ b/integrations/teams/src/markdown/teams-html-to-markdown.test.ts @@ -0,0 +1,312 @@ +import { describe, expect, test } from 'vitest' +import { TestCase } from '../types' +import { transformTeamsHtmlToStdMarkdown } from './teams-html-to-markdown' + +type TeamsHtmlToMarkdownTestCase = TestCase | TestCase + +const teamsHtmlToMarkdownTestCases: TestCase[] = ( + [ + // ----- Bold ----- + { + input: ['Bold', 'Bold'], + expects: '**Bold**', + description: 'Convert bold tag to markdown', + }, + // ----- Italic ----- + { + input: ['Italic', 'Italic'], + expects: '_Italic_', + description: 'Convert italic tag to markdown', + }, + // ----- Strikethrough ----- + { + input: ['Strike', 'Strike'], + expects: '~~Strike~~', + description: 'Convert strikethrough tag to markdown', + }, + // ----- Headings & Paragraph ----- + { + input: '

    Heading 1

    ', + expects: '# Heading 1', + description: 'Convert h1 tag to heading markdown', + }, + { + input: '

    Heading 2

    ', + expects: '## Heading 2', + description: 'Convert h2 tag to heading markdown', + }, + { + input: '

    Heading 3

    ', + expects: '### Heading 3', + description: 'Convert h3 tag to heading markdown', + }, + { + input: '

    Heading 4

    ', + expects: '#### Heading 4', + // Though h4 is not supported by MS Teams, this + // converter should still handle it correctly + description: 'Convert h4 tag to heading markdown', + }, + { + input: '
    Heading 5
    ', + expects: '##### Heading 5', + // Though h5 is not supported by MS Teams, this + // converter should still handle it correctly + description: 'Convert h5 tag to heading markdown', + }, + { + input: '
    Heading 6
    ', + expects: '###### Heading 6', + // Though h6 is not supported by MS Teams, this + // converter should still handle it correctly + description: 'Convert h6 tag to heading markdown', + }, + { + input: '

    Paragraph

    ', + expects: 'Paragraph', + description: 'Convert paragraph tag to markdown', + }, + // ----- Horizontal Rule ----- + { + input: 'Horizontal Rule
    ', + expects: 'Horizontal Rule\n\n---', + // Requires 2 new lines before horizontal rule + // markdown otherwise it becomes a heading mark + description: 'Convert horizontal rule tag to markdown', + }, + // ----- Inline Code ----- + { + input: 'Code Snippet', + expects: '`Code Snippet`', + description: 'Convert inline code html to markdown', + }, + // ----- Code Blocks ----- + { + input: '
    console.log("Code Block")
    ', + expects: '```\nconsole.log("Code Block")\n```', + description: 'Convert code block html to markdown - Without language', + }, + { + input: '
    console.log("Code Block")
    ', + expects: '```javascript\nconsole.log("Code Block")\n```', + description: 'Convert code block html to markdown - With language', + }, + // ----- Lists ----- + { + input: '
    • Item 1
    • Item 2
    • Item 3
    ', + expects: '- Item 1\n- Item 2\n- Item 3', + description: 'Convert unordered list HTML to markdown list', + }, + { + input: '
    1. Item 1
    2. Item 2
    3. Item 3
    ', + expects: '1. Item 1\n2. Item 2\n3. Item 3', + description: 'Convert ordered list HTML to markdown list', + }, + // ----- Tables ----- + { + input: [ + // With thead/tbody + '
    Header 1Header 2Header 3
    Row 1, Cell 1Row 1, Cell 2Row 1, Cell 3
    Row 2, Cell 1Row 2, Cell 2Row 2, Cell 3
    Row 3, Cell 1Row 3, Cell 2Row 3, Cell 3
    ', + // Without thead/tbody + '
    Header 1Header 2Header 3
    Row 1, Cell 1Row 1, Cell 2Row 1, Cell 3
    Row 2, Cell 1Row 2, Cell 2Row 2, Cell 3
    Row 3, Cell 1Row 3, Cell 2Row 3, Cell 3
    ', + // Actual MS Teams table created by user + '

     

    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n

    Header 1

    \n
    \n

    Header 2

    \n
    \n

    Header 3

    \n
    \n

    Row 1, Cell 1

    \n
    \n

    Row 1, Cell 2

    \n
    \n

    Row 1, Cell 3

    \n
    \n

    Row 2, Cell 1

    \n
    \n

    Row 2, Cell 2

    \n
    \n

    Row 2, Cell 3

    \n
    \n

    Row 3, Cell 1

    \n
    \n

    Row 3, Cell 2

    \n
    \n

    Row 3, Cell 3

    \n
    \n

     

    ', + ], + expects: + '| Header 1 | Header 2 | Header 3 |\n| --- | --- | --- |\n| Row 1, Cell 1 | Row 1, Cell 2 | Row 1, Cell 3 |\n| Row 2, Cell 1 | Row 2, Cell 2 | Row 2, Cell 3 |\n| Row 3, Cell 1 | Row 3, Cell 2 | Row 3, Cell 3 |', + // NOTE: This converter cannot support "Editable"/"FluidEmbedCard" tables since there is no way to access the content of those cards (Last checked: 2025-11-25) + description: 'Convert table HTML to markdown table', + }, + // ----- Blockquote ----- + { + input: [ + '
    \n\nBlockquote\n
    ', + // Actual Teams HTML output for blockquote + '
    \n

    Blockquote

    \n
    \n

     

    ', + ], + expects: '> Blockquote', + description: 'Apply blockquote markdown to text', + }, + // ----- Hyperlinks ----- + { + input: 'Hyperlink', + expects: '[Hyperlink](https://www.botpress.com/)', + description: 'Convert hyperlink html tag without title attribute to markdown', + }, + { + input: 'Hyperlink', + expects: '[Hyperlink](https://www.botpress.com/ "Test Title")', + description: 'Convert hyperlink html tag with title attribute to markdown', + }, + // ----- Telephone Hyperlinks ----- + { + input: 'Phone Number', + expects: '[Phone Number](tel:1234567890)', + // Though telephone links are not currently supported by + // MS Teams, this converter should still handle it correctly + description: 'Convert telephone link tag without title attribute to markdown', + }, + { + input: 'Phone Number', + expects: '[Phone Number](tel:1234567890 "Telephone Title")', + // Though telephone links are not currently supported by + // MS Teams, this converter should still handle it correctly + description: 'Convert telephone link tag with title attribute to markdown', + }, + // ----- Email Hyperlinks ----- + { + input: 'Test Email Address', + expects: '[Test Email Address](mailto:test@botpress.com)', + description: 'Convert email address link tag without title attribute to markdown', + }, + { + input: [ + 'Test Email Address', + // Actual Teams HTML output for email address + '

    Test Email Address

    ', + ], + expects: '[Test Email Address](mailto:test@botpress.com "mailto:test@botpress.com")', + description: 'Convert email address link tag with title attribute to markdown', + }, + // ----- Image ----- + { + input: [ + 'image', + // Actual Teams HTML output for image (with "src" & "itemid" attribute values anonymized) + '

     

    \n

    image

    ', + ], + expects: '![image](https://shop.botpress.com/cdn/shop/files/logo.png?v=1708026010)', + description: 'Convert image html tag without title attribute to markdown', + }, + { + input: [ + 'image', + // Actual Teams HTML output for image (with "src" & "itemid" attribute values anonymized & title attribute added) + '

     

    \n

    image

    ', + ], + expects: '![image](https://shop.botpress.com/cdn/shop/files/logo.png?v=1708026010 "Test Title")', + description: 'Convert image html tag with title attribute to markdown', + }, + // ----- Unsupported tags ----- + { + input: '

    Underline

    ', + expects: 'Underline', + // The standard markdown spec does not support underlines + description: 'Do not apply unsupported underline effect', + }, + // ===== Advanced test cases ===== + { + input: 'abcdefghHello New World', + expects: '**Hello** New ~~World~~', + description: 'Convert non-overlapping effects with gap into separate markdown effects', + }, + { + input: 'Multiple Effects', + expects: '~~**Multiple Effects**~~', + description: 'Convert nested effects on the same range correctly', + }, + { + input: '

    Hello New World

    ', + expects: '**Hello** _**New** World_', + description: 'Convert encapsulated nested effects corrrectly', + }, + { + input: 'Multiple Effects', + expects: '_~~**Multiple Effects**~~_', + description: 'Convert multiple effects on the same range into nested markdown effects correctly', + }, + { + input: 'Unterminated Bold', + expects: '**Unterminated Bold**', + description: 'Convert bold html tag without closing tag to markdown', + }, + { + input: '
    Hello
    \nNothing\n
    More Quotes!
    ', + expects: '> Hello\n\nNothing\n\n> More Quotes!', + description: 'Apply blockquote markdown to multiple lines, with non-quote line in between', + }, + { + input: '
    Hello Quote World
    ', + expects: '> Hello **Quote** World', + description: + // An incorrect outcome of this test case would be "> Hello **> Quote**> World" + "Ensure any effect nested within blockquote tag doesn't create multiple blockquote marks (It should only ever be at the start of a line)", + }, + { + input: '
    \n

    Blockquote 1
    \nBlockquote 2
    \nBlockquote 3

    \n
    \n

     

    ', + expects: '> Blockquote 1\n> Blockquote 2\n> Blockquote 3', + description: 'Multiline blockquote produces a blockquote mark for each line', + }, + { + input: '
    \n

    Quote Line 1
    \n
    \nQuote Line 3

    \n
    \n

     

    ', + expects: '> Quote Line 1\n> \n> Quote Line 3', + description: 'Multiline blockquote produces a blockquote mark for each line, with empty lines', + }, + { + input: [ + '
    Quote Line 1
    \n
    \nQuote Line 3\n

     

    \n
    \n

     

    ', + '
    \n

    Quote Line 1
    \n
    \nQuote Line 3

    \n
    \n

     

    ', + ], + expects: '> **Quote Line 1**\n> \n> **Quote Line 3**', + description: + 'Multiline blockquote produces a blockquote mark for each line, with empty lines and intersecting effect', + }, + { + input: '

    Some Hyperlink

    ', + expects: '[Some **Hyperlink**](https://www.botpress.com/)', + description: 'Convert hyperlink with partial bold effect on link text to markdown (without title attribute)', + }, + { + input: + '

    Some Hyperlink

    ', + expects: '[Some **Hyperlink**](https://www.botpress.com/ "https://www.botpress.com/")', + description: 'Convert hyperlink with partial bold effect on link text to markdown (with title attribute)', + }, + { + input: 'Click Me', + expects: 'Click Me', + description: 'Strip hyperlink with XSS logic in href attribute', + }, + { + input: 'alt text', + expects: '![alt text](image.jpg)', + description: 'Strip image with XSS logic in onerror attribute', + }, + ] as TeamsHtmlToMarkdownTestCase[] +).reduce( + (testCases, testCase) => { + const { input, expects, description } = testCase + if (typeof input === 'string') { + return testCases.concat({ + input, + expects, + description, + }) + } else if (Array.isArray(input)) { + return testCases.concat( + input.map((inputVariant) => { + return { + input: inputVariant, + expects, + description, + } + }) + ) + } + + return testCases + }, + [] as TestCase[] +) + +describe('Teams Html to Markdown Conversion', () => { + test.each(teamsHtmlToMarkdownTestCases)('$description', ({ input, expects }: TestCase) => { + const markdown = transformTeamsHtmlToStdMarkdown(input) + expect(markdown).toBe(expects) + }) +}) diff --git a/integrations/teams/src/markdown/teams-html-to-markdown.ts b/integrations/teams/src/markdown/teams-html-to-markdown.ts new file mode 100644 index 00000000000..2e22eb5ac46 --- /dev/null +++ b/integrations/teams/src/markdown/teams-html-to-markdown.ts @@ -0,0 +1,29 @@ +import TurndownService from 'turndown' +import { defaultSanitizeConfig, sanitizeHtml } from './sanitize-utils' +import { customPlugin } from './turndown-rules/custom' +import { gfm } from './turndown-rules/gfm' + +export function transformTeamsHtmlToStdMarkdown(teamsHtml: string) { + const turndownService = new TurndownService({ + codeBlockStyle: 'fenced', + headingStyle: 'atx', + bulletListMarker: '-', + hr: '\n\n---', + preformattedCode: true, + }) + + gfm(turndownService) + customPlugin(turndownService) + + const sanitizedHtml = sanitizeHtml(teamsHtml, { + allowedSchemes: defaultSanitizeConfig.allowedSchemes.concat('tel'), + }) + return ( + turndownService + .turndown(sanitizedHtml) + // Remove trailing spaces added before line breaks (Side effect of "
    \n") + .replace(/ \n/g, '\n') + .replace(/\n{3,}/g, '\n\n') + .replace(/ {2,}/g, ' ') + ) +} diff --git a/integrations/teams/src/markdown/turndown-rules/common.ts b/integrations/teams/src/markdown/turndown-rules/common.ts new file mode 100644 index 00000000000..f8f07b8de15 --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/common.ts @@ -0,0 +1,6 @@ +export const isElementOfType = ( + el: Element, + tagName: K +): el is HTMLElementTagNameMap[K] => { + return el.tagName.toLowerCase() === tagName.toLowerCase() +} diff --git a/integrations/teams/src/markdown/turndown-rules/custom/code-block-with-language.ts b/integrations/teams/src/markdown/turndown-rules/custom/code-block-with-language.ts new file mode 100644 index 00000000000..42e8272b91f --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/custom/code-block-with-language.ts @@ -0,0 +1,19 @@ +import TurndownService from 'turndown' +import { isElementOfType } from '../common' + +const languageRegExp = /language-(\w+)/ + +// The "turndown-plugin-gfm" package did not support parsing code blocks with language classes. +export const codeBlockWithLanguage = (turndownService: TurndownService) => { + turndownService.addRule('codeBlockWithLanguage', { + filter: (node: HTMLElement): boolean => { + const firstChild = node.firstElementChild + return isElementOfType(node, 'pre') && firstChild !== null && isElementOfType(firstChild, 'code') + }, + replacement(_: string, node: HTMLElement, options: TurndownService.Options) { + const className = node.className ?? '' + const language = className.match(languageRegExp)?.[1] ?? '' + return options.fence + language + '\n' + node.textContent + '\n' + options.fence + }, + }) +} diff --git a/integrations/teams/src/markdown/turndown-rules/custom/index.ts b/integrations/teams/src/markdown/turndown-rules/custom/index.ts new file mode 100644 index 00000000000..57fdb4481e3 --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/custom/index.ts @@ -0,0 +1,7 @@ +import TurndownService from 'turndown' +import { codeBlockWithLanguage } from './code-block-with-language' +import { stripUnderline } from './underline' + +export const customPlugin = (turndownService: TurndownService) => { + turndownService.use([stripUnderline, codeBlockWithLanguage]) +} diff --git a/integrations/teams/src/markdown/turndown-rules/custom/underline.ts b/integrations/teams/src/markdown/turndown-rules/custom/underline.ts new file mode 100644 index 00000000000..1be5a11c29f --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/custom/underline.ts @@ -0,0 +1,10 @@ +import TurndownService from 'turndown' + +export const stripUnderline = (turndownService: TurndownService) => { + turndownService.addRule('underline', { + filter: ['u'], + replacement(content) { + return content + }, + }) +} diff --git a/integrations/teams/src/markdown/turndown-rules/gfm/highlighted-code-block.ts b/integrations/teams/src/markdown/turndown-rules/gfm/highlighted-code-block.ts new file mode 100644 index 00000000000..924abd8c44f --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/gfm/highlighted-code-block.ts @@ -0,0 +1,23 @@ +import type TurndownService from 'turndown' + +const highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/ + +export const highlightedCodeBlock = (turndownService: TurndownService) => { + turndownService.addRule('highlightedCodeBlock', { + filter(node: HTMLElement): boolean { + const firstChild = node.firstChild + return ( + node.nodeName === 'DIV' && + highlightRegExp.test(node.className) && + firstChild !== null && + firstChild.nodeName === 'PRE' + ) + }, + replacement(_, node, options) { + const className = node.className || '' + const language = (className.match(highlightRegExp) || [null, ''])[1] + + return '\n\n' + options.fence + language + '\n' + node.firstChild?.textContent + '\n' + options.fence + '\n\n' + }, + }) +} diff --git a/integrations/teams/src/markdown/turndown-rules/gfm/index.ts b/integrations/teams/src/markdown/turndown-rules/gfm/index.ts new file mode 100644 index 00000000000..f80eba084c4 --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/gfm/index.ts @@ -0,0 +1,15 @@ +import TurndownService from 'turndown' +import { highlightedCodeBlock } from './highlighted-code-block' +import { strikethrough } from './strikethrough' +import { tables } from './tables' +import { taskListItems } from './task-list-items' + +/** This plugin is mostly pulled from the source code of the "turndown-plugin-gfm" npm package. + * The reason we are not using the package directly is that it doesn't support ES6 imports. + * + * @remark The source code was slightly modified to work with TypeScript. + * @see [NPM Package](https://www.npmjs.com/package/turndown-plugin-gfm) + * @see [Repository](https://github.com/mixmark-io/turndown-plugin-gfm) */ +export const gfm = (turndownService: TurndownService) => { + turndownService.use([highlightedCodeBlock, strikethrough, tables, taskListItems]) +} diff --git a/integrations/teams/src/markdown/turndown-rules/gfm/strikethrough.ts b/integrations/teams/src/markdown/turndown-rules/gfm/strikethrough.ts new file mode 100644 index 00000000000..8f0c336c7fc --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/gfm/strikethrough.ts @@ -0,0 +1,10 @@ +import TurndownService from 'turndown' + +export const strikethrough = (turndownService: TurndownService) => { + turndownService.addRule('strikethrough', { + filter: ['del', 's'], + replacement(content) { + return `~~${content}~~` + }, + }) +} diff --git a/integrations/teams/src/markdown/turndown-rules/gfm/tables.ts b/integrations/teams/src/markdown/turndown-rules/gfm/tables.ts new file mode 100644 index 00000000000..98511de442c --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/gfm/tables.ts @@ -0,0 +1,94 @@ +import type TurndownService from 'turndown' +import { isElementOfType } from '../common' + +const rules: Record = {} + +rules.tableCell = { + filter: ['th', 'td'], + replacement(content: string, node: HTMLElement) { + return cell(content, node).replace(/\n/g, '') + }, +} + +rules.tableRow = { + filter: 'tr', + replacement(content: string, node: HTMLElement) { + let borderCells = '' + const alignMap: Record = { left: ':--', right: '--:', center: ':-:' } + + if (isHeadingRow(node)) { + Array.from(node.children).forEach((child) => { + let border = '---' + const align = (child.getAttribute('align') || '').toLowerCase() + + if (align) border = alignMap[align] || border + + borderCells += cell(border, child) + }) + } else { + } + return '\n' + content + (borderCells ? '\n' + borderCells : '') + }, +} + +const _isTableWithHeadingRow = (node: Element) => { + if (!isElementOfType(node, 'table')) return false + const firstRow = node.rows[0] + + return !!firstRow && isHeadingRow(firstRow) +} + +rules.table = { + // Only convert tables with a heading row. + // Tables with no heading row are kept using `keep` (see below). + filter: (node) => _isTableWithHeadingRow(node), + replacement(content) { + // Ensure there are no blank lines + content = content.replace('\n\n', '\n') + return '\n\n' + content + '\n\n' + }, +} + +rules.tableSection = { + filter: ['thead', 'tbody', 'tfoot'], + replacement(content: string) { + return content + }, +} + +// A tr is a heading row if: +// - the parent is a THEAD +// - or if its the first child of the TABLE or the first TBODY (possibly +// following a blank THEAD) +// - and every cell is a TH +function isHeadingRow(tr: HTMLElement) { + const parentNode = tr.parentNode + + return ( + parentNode?.nodeName === 'THEAD' || + (parentNode?.firstChild === tr && (parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode))) + ) +} + +function isFirstTbody(element: Node) { + const previousSibling = element.previousSibling + return ( + element.nodeName === 'TBODY' && + (!previousSibling || (previousSibling.nodeName === 'THEAD' && /^\s*$/i.test(previousSibling.textContent ?? ''))) + ) +} + +function cell(content: string, node: Element) { + const childNodes = Array.from(node.parentNode?.childNodes ?? []) + const index = childNodes.indexOf(node) + let prefix = ' ' + if (index === 0) prefix = '| ' + return prefix + content + ' |' +} + +export const tables = (turndownService: TurndownService) => { + turndownService.keep((node) => !isElementOfType(node, 'table')) + for (const [key, rule] of Object.entries(rules)) { + turndownService.addRule(key, rule) + } +} diff --git a/integrations/teams/src/markdown/turndown-rules/gfm/task-list-items.ts b/integrations/teams/src/markdown/turndown-rules/gfm/task-list-items.ts new file mode 100644 index 00000000000..851e3f99cec --- /dev/null +++ b/integrations/teams/src/markdown/turndown-rules/gfm/task-list-items.ts @@ -0,0 +1,17 @@ +import TurndownService from 'turndown' +import { isElementOfType } from '../common' + +export const taskListItems = (turndownService: TurndownService) => { + turndownService.addRule('taskListItems', { + filter(node: HTMLElement): boolean { + return isElementOfType(node, 'input') && node.type === 'checkbox' && node.parentNode?.nodeName === 'LI' + }, + replacement(_content: string, node: HTMLElement) { + if (!isElementOfType(node, 'input')) { + return '' + } + + return `${node.checked ? '[x]' : '[ ]'} ` + }, + }) +} diff --git a/integrations/teams/src/markdown/types.ts b/integrations/teams/src/markdown/types.ts new file mode 100644 index 00000000000..cdf59c56295 --- /dev/null +++ b/integrations/teams/src/markdown/types.ts @@ -0,0 +1,38 @@ +import type { Node, LinkReference, Parent, Nodes, Definition, TableCell } from 'mdast' + +export type Merge = Omit & R + +export type DefinitionNodeData = { + identifier: string + url: string + label?: string | null + title?: string | null +} + +export type DefinedLinkReference = LinkReference & { linkDefinition?: DefinitionNodeData } +export type TableCellWithHeaderInfo = TableCell & { isHeader?: boolean } + +export type NodeHandler = ( + node: Type extends Node ? Type : Extract, + visit: (node: Parent) => string, + parents: Parent[], + handlers: MarkdownHandlers, + definitions: Record +) => string + +export type MarkdownHandlers = Partial< + Merge< + { [Type in Nodes['type']]: NodeHandler }, + { + linkReference: NodeHandler + tableCell: NodeHandler + } + > +> + +type Is = A extends B ? (B extends A ? true : false) : false +type HasProperties = Is, Required>>> +type Expect<_T extends true> = void + +// This builds only if the Definition contains all properties of DefinitionNodeData +type _Test = Expect> diff --git a/integrations/teams/src/microsoft-api/index.ts b/integrations/teams/src/microsoft-api/index.ts new file mode 100644 index 00000000000..3a6536c71ac --- /dev/null +++ b/integrations/teams/src/microsoft-api/index.ts @@ -0,0 +1,37 @@ +import { ClientSecretCredential } from '@azure/identity' +import { Client } from '@microsoft/microsoft-graph-client' +import * as bp from '.botpress' + +export class MicrosoftClient { + private _graphClient: Client + + private constructor(ctx: bp.Context) { + const { tenantId, appId, appPassword } = ctx.configuration + + const credential = new ClientSecretCredential(tenantId as string, appId, appPassword) + + this._graphClient = Client.initWithMiddleware({ + authProvider: { + getAccessToken: async () => { + const token = await credential.getToken('https://graph.microsoft.com/.default') + return token?.token! + }, + }, + }) + } + + public async getUserByEmail(email: string) { + try { + return await this._graphClient.api(`/users/${email}`).get() + } catch (err: any) { + if (err.statusCode === 404) { + throw new Error(`No user found with email: ${email}`) + } + throw err + } + } + + public static create(ctx: bp.Context) { + return new MicrosoftClient(ctx) + } +} diff --git a/integrations/teams/src/schemas.ts b/integrations/teams/src/schemas.ts new file mode 100644 index 00000000000..2bea0343cdd --- /dev/null +++ b/integrations/teams/src/schemas.ts @@ -0,0 +1,16 @@ +import { z } from '@botpress/sdk' +import { channelAccountSchema } from 'definitions/states' + +export const teamsActivitySchema = z + .object({ + id: z.string().trim().min(1), + type: z.string(), + from: channelAccountSchema.passthrough().partial().optional(), + conversation: z + .object({ + id: z.string().trim().min(1), + }) + .passthrough(), + text: z.string().optional(), + }) + .passthrough() diff --git a/integrations/teams/src/setup.ts b/integrations/teams/src/setup.ts new file mode 100644 index 00000000000..58c6d7ff70a --- /dev/null +++ b/integrations/teams/src/setup.ts @@ -0,0 +1,22 @@ +import { RuntimeError } from '@botpress/sdk' +import axios, { isAxiosError } from 'axios' +import * as bp from '.botpress' + +export const register: bp.Integration['register'] = async ({ ctx }) => { + const tenant = ctx.configuration.tenantId ?? 'botframework.com' + + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: ctx.configuration.appId, + client_secret: ctx.configuration.appPassword, + tenant_id: tenant, + scope: 'https://api.botframework.com/.default', + }) + + await axios.post(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, params.toString()).catch((e) => { + const message = isAxiosError(e) ? e.response?.data?.error_description : e.message + throw new RuntimeError(`Failed to authenticate with Microsoft Teams: ${message}`) + }) +} + +export const unregister: bp.Integration['unregister'] = async () => {} diff --git a/integrations/teams/src/types.ts b/integrations/teams/src/types.ts new file mode 100644 index 00000000000..732feff85a5 --- /dev/null +++ b/integrations/teams/src/types.ts @@ -0,0 +1,6 @@ +export type TestCase = { + input: INPUT + expects: EXPECTED + description: string + skip?: boolean +} diff --git a/integrations/teams/src/webhooks/handler.ts b/integrations/teams/src/webhooks/handler.ts new file mode 100644 index 00000000000..cf589514208 --- /dev/null +++ b/integrations/teams/src/webhooks/handler.ts @@ -0,0 +1,26 @@ +import { Activity } from 'botbuilder' +import { processInboundChannelMessage } from '../channels/inbound' +import { teamsActivitySchema } from '../schemas' +import { authorizeRequest } from './signature' +import * as bp from '.botpress' + +export const handler: bp.IntegrationProps['handler'] = async (props) => { + const { req } = props + await authorizeRequest(req) + + if (!req.body) { + props.logger.forBot().warn('Handler received an empty body') + return + } + + const parsedBody: Object = JSON.parse(req.body) + const result = teamsActivitySchema.safeParse(parsedBody) + if (!result.success) { + props.logger.forBot().error('Invalid activity payload received:', result.error.format()) + return + } + + const activity = result.data as unknown as Activity + props.logger.forBot().info(`Handler received event of type ${activity.type}`) + await processInboundChannelMessage(props, activity) +} diff --git a/integrations/teams/src/signature.ts b/integrations/teams/src/webhooks/signature.ts similarity index 87% rename from integrations/teams/src/signature.ts rename to integrations/teams/src/webhooks/signature.ts index 3394fdd055d..3e64d42fcf4 100644 --- a/integrations/teams/src/signature.ts +++ b/integrations/teams/src/webhooks/signature.ts @@ -6,7 +6,7 @@ const jwksClient = new JwksClient({ jwksUri: 'https://login.botframework.com/v1/.well-known/keys', }) -const getKey = (header: JwtHeader, callback: SigningKeyCallback) => { +const _getKey = (header: JwtHeader, callback: SigningKeyCallback) => { jwksClient.getSigningKey(header.kid, function (err, key) { if (key) { callback(null, key.getPublicKey()) @@ -27,7 +27,7 @@ export const authorizeRequest = async (req: Request) => { } return new Promise((resolve, reject) => { - jwt.verify(token, getKey, (err, decoded) => { + jwt.verify(token, _getKey, (err, decoded) => { if (err) { reject(err) } else { diff --git a/integrations/twilio/integration.definition.ts b/integrations/twilio/integration.definition.ts index 827006245f0..6a6cc87b104 100644 --- a/integrations/twilio/integration.definition.ts +++ b/integrations/twilio/integration.definition.ts @@ -4,9 +4,10 @@ import proactiveConversation from 'bp_modules/proactive-conversation' import proactiveUser from 'bp_modules/proactive-user' export const INTEGRATION_NAME = 'twilio' +export const INTEGRATION_VERSION = '1.0.4' export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '1.0.2', + version: INTEGRATION_VERSION, title: 'Twilio', description: 'Send and receive messages, voice calls, emails, SMS, and more.', icon: 'icon.svg', @@ -36,7 +37,7 @@ export default new IntegrationDefinition({ channel: { title: 'Conversation Channel', description: 'A channel for sending and receiving messages through Twilio Conversations', - messages: messages.defaults, + messages: { ...messages.defaults, bloc: messages.markdownBloc }, message: { tags: { id: { diff --git a/integrations/twilio/src/index.ts b/integrations/twilio/src/index.ts index 48805e982c8..88fb2a95fba 100644 --- a/integrations/twilio/src/index.ts +++ b/integrations/twilio/src/index.ts @@ -4,7 +4,7 @@ import * as sdk from '@botpress/sdk' import { sentry as sentryHelpers } from '@botpress/sdk-addons' import axios from 'axios' import * as crypto from 'crypto' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import queryString from 'query-string' import { Twilio } from 'twilio' import { transformMarkdownForTwilio } from './markdown-to-twilio' @@ -412,7 +412,7 @@ async function sendMessage({ ctx, conversation, ack, mediaUrl, text, logger }: S event: 'unhandled_markdown', properties: { errMsg }, }, - { integrationName: INTEGRATION_NAME, key: bp.secrets.POSTHOG_KEY } + { integrationName: INTEGRATION_NAME, integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY } ) } } diff --git a/integrations/viber/integration.definition.ts b/integrations/viber/integration.definition.ts index b4421f9f00e..96e1554e0be 100644 --- a/integrations/viber/integration.definition.ts +++ b/integrations/viber/integration.definition.ts @@ -3,7 +3,7 @@ import { sentry as sentryHelpers } from '@botpress/sdk-addons' export default new IntegrationDefinition({ name: 'viber', - version: '1.0.2', + version: '1.0.3', title: 'Viber', description: 'Send and receive SMS messages.', icon: 'icon.svg', @@ -19,7 +19,7 @@ export default new IntegrationDefinition({ channel: { title: 'Viber conversation', description: 'Channel for a Viber conversation', - messages: { ...messages.defaults }, + messages: { ...messages.defaults, bloc: messages.markdownBloc }, message: { tags: { id: { title: 'Message ID', description: 'Viber message ID' }, diff --git a/integrations/vonage/integration.definition.ts b/integrations/vonage/integration.definition.ts index 906ea9f9435..6ff5ec5376b 100644 --- a/integrations/vonage/integration.definition.ts +++ b/integrations/vonage/integration.definition.ts @@ -5,7 +5,7 @@ import proactiveUser from 'bp_modules/proactive-user' export default new IntegrationDefinition({ name: 'vonage', - version: '1.0.1', + version: '1.0.2', title: 'Vonage', description: 'Send and receive SMS messages.', icon: 'icon.svg', @@ -22,7 +22,7 @@ export default new IntegrationDefinition({ channel: { title: 'Channel', description: 'The vonage Channel', - messages: { ...messages.defaults }, + messages: { ...messages.defaults, bloc: messages.markdownBloc }, message: { tags: { id: { title: 'ID', description: 'The id of the message' }, diff --git a/integrations/whatsapp/integration.definition.ts b/integrations/whatsapp/integration.definition.ts index fd7a2304914..fabf038be8b 100644 --- a/integrations/whatsapp/integration.definition.ts +++ b/integrations/whatsapp/integration.definition.ts @@ -93,9 +93,10 @@ const defaultBotPhoneNumberId = { } export const INTEGRATION_NAME = 'whatsapp' +export const INTEGRATION_VERSION = '4.5.19' export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '4.5.18', + version: INTEGRATION_VERSION, title: 'WhatsApp', description: 'Send and receive messages through WhatsApp.', icon: 'icon.svg', diff --git a/integrations/whatsapp/src/actions/start-conversation.ts b/integrations/whatsapp/src/actions/start-conversation.ts index 86ae585e047..5dcaabeddfd 100644 --- a/integrations/whatsapp/src/actions/start-conversation.ts +++ b/integrations/whatsapp/src/actions/start-conversation.ts @@ -1,6 +1,6 @@ import { isApiError } from '@botpress/client' import { posthogHelper } from '@botpress/common' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import { BodyComponent, BodyParameter, Language, Template } from 'whatsapp-api-js/messages' import { getDefaultBotPhoneNumberId, getAuthenticatedWhatsappClient } from '../auth' import { formatPhoneNumber } from '../misc/phone-number-to-whatsapp' @@ -54,7 +54,7 @@ export const startConversation: bp.IntegrationProps['actions']['startConversatio phoneNumber: userPhone, }, }, - { integrationName: INTEGRATION_NAME, key: bp.secrets.POSTHOG_KEY } + { integrationName: INTEGRATION_NAME, integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY } ) const errorMessage = (thrown instanceof Error ? thrown : new Error(String(thrown))).message logForBotAndThrow(`Failed to parse phone number "${userPhone}": ${errorMessage}`, logger) diff --git a/integrations/whatsapp/src/channels/channel.ts b/integrations/whatsapp/src/channels/channel.ts index 4cc85bcd971..2f34ec3d41b 100644 --- a/integrations/whatsapp/src/channels/channel.ts +++ b/integrations/whatsapp/src/channels/channel.ts @@ -1,5 +1,5 @@ import { posthogHelper } from '@botpress/common' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import { Text, Audio, @@ -50,7 +50,7 @@ export const channel: bp.IntegrationProps['channels']['channel'] = { messageId: props.message.id, }, }, - { integrationName: INTEGRATION_NAME, key: bp.secrets.POSTHOG_KEY } + { integrationName: INTEGRATION_NAME, integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY } ) props.logger.forBot().warn(`Message ${props.message.id} skipped: no message to send.`) return diff --git a/integrations/whatsapp/src/index.ts b/integrations/whatsapp/src/index.ts index b64e986199a..a50a62257e2 100644 --- a/integrations/whatsapp/src/index.ts +++ b/integrations/whatsapp/src/index.ts @@ -1,5 +1,5 @@ import { posthogHelper } from '@botpress/common' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import actions from './actions' import channels from './channels' import { register, unregister } from './setup' @@ -8,6 +8,7 @@ import * as bp from '.botpress' @posthogHelper.wrapIntegration({ integrationName: INTEGRATION_NAME, + integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY, }) class WhatsappIntegration extends bp.Integration { diff --git a/integrations/whatsapp/src/webhook/handlers/messages.ts b/integrations/whatsapp/src/webhook/handlers/messages.ts index 955053a9688..42da8acd957 100644 --- a/integrations/whatsapp/src/webhook/handlers/messages.ts +++ b/integrations/whatsapp/src/webhook/handlers/messages.ts @@ -2,7 +2,7 @@ import { RuntimeError, isApiError } from '@botpress/client' import { posthogHelper } from '@botpress/common' import { ValueOf } from '@botpress/sdk/dist/utils/type-utils' import axios from 'axios' -import { INTEGRATION_NAME } from 'integration.definition' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import { getAccessToken, getAuthenticatedWhatsappClient } from '../../auth' import { formatPhoneNumber } from '../../misc/phone-number-to-whatsapp' import { WhatsAppMessage, WhatsAppMessageValue } from '../../misc/types' @@ -53,7 +53,7 @@ async function _handleIncomingMessage( phoneNumber: message.from, }, }, - { integrationName: INTEGRATION_NAME, key: bp.secrets.POSTHOG_KEY } + { integrationName: INTEGRATION_NAME, integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY } ) const errorMessage = thrown instanceof Error ? thrown.message : String(thrown) logger.error(`Failed to parse phone number "${message.from}": ${errorMessage}`) diff --git a/integrations/zendesk/integration.definition.ts b/integrations/zendesk/integration.definition.ts index 845a368b0a8..fcde0a1620e 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: '2.8.5', + version: '2.8.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/interfaces/hitl/interface.definition.ts b/interfaces/hitl/interface.definition.ts index 6d1ca69aa1a..ddf7aafd89f 100644 --- a/interfaces/hitl/interface.definition.ts +++ b/interfaces/hitl/interface.definition.ts @@ -1,6 +1,6 @@ import * as sdk from '@botpress/sdk' -type AnyMessageType = (typeof sdk.messages.defaults)[keyof typeof sdk.messages.defaults] +type AnyMessageType = { schema: sdk.z.ZodObject } const withUserId = (s: AnyMessageType) => ({ ...s, schema: () => @@ -17,6 +17,7 @@ const messageSourceSchema = sdk.z.union([ const allMessages = { ...sdk.messages.defaults, markdown: sdk.messages.markdown, + bloc: sdk.messages.markdownBloc, } satisfies Record type Tuple = [T, T, ...T[]] @@ -182,7 +183,7 @@ export default new sdk.InterfaceDefinition({ audio: withUserId(sdk.messages.defaults.audio), video: withUserId(sdk.messages.defaults.video), file: withUserId(sdk.messages.defaults.file), - bloc: withUserId(sdk.messages.defaults.bloc), + bloc: withUserId(sdk.messages.markdownBloc), // TODO: use the actual bloc message when bumping a version of the interface }, }, }, diff --git a/packages/chat-api/src/version.ts b/packages/chat-api/src/version.ts index 37c13819ff7..6e013080919 100644 --- a/packages/chat-api/src/version.ts +++ b/packages/chat-api/src/version.ts @@ -1 +1 @@ -export const apiVersion = '0.7.3' +export const apiVersion = '0.7.4' diff --git a/packages/cli/package.json b/packages/cli/package.json index a27e849ad7e..05162efdf0f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "4.27.3", + "version": "4.27.4", "description": "Botpress CLI", "scripts": { "build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen", @@ -27,7 +27,7 @@ "@apidevtools/json-schema-ref-parser": "^11.7.0", "@botpress/chat": "0.5.4", "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.2", + "@botpress/sdk": "4.21.0", "@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 530ed23adf7..52aa68ec99f 100644 --- a/packages/cli/templates/empty-bot/package.json +++ b/packages/cli/templates/empty-bot/package.json @@ -6,7 +6,7 @@ "private": true, "dependencies": { "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.2" + "@botpress/sdk": "4.21.0" }, "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 b51fa09fa11..1a23ed29fc9 100644 --- a/packages/cli/templates/empty-integration/package.json +++ b/packages/cli/templates/empty-integration/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.2" + "@botpress/sdk": "4.21.0" }, "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 837f82d82fc..e744ef5d67a 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": "4.20.2" + "@botpress/sdk": "4.21.0" }, "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 67ae35691e7..ee8c00762bd 100644 --- a/packages/cli/templates/hello-world/package.json +++ b/packages/cli/templates/hello-world/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.2" + "@botpress/sdk": "4.21.0" }, "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 403362aa1f3..56018259f47 100644 --- a/packages/cli/templates/webhook-message/package.json +++ b/packages/cli/templates/webhook-message/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.2", + "@botpress/sdk": "4.21.0", "axios": "^1.6.8" }, "devDependencies": { diff --git a/packages/common/package.json b/packages/common/package.json index 3b50476ca04..8879dbd13fb 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -11,7 +11,7 @@ "dedent": "^1.6.0", "marked": "^15.0.1", "openai": "^6.9.0", - "posthog-node": "5.11.2", + "posthog-node": "5.14.1", "preact": "^10.26.6", "preact-render-to-string": "^6.5.13", "remark": "^15.0.1", diff --git a/packages/common/src/posthog/helper.ts b/packages/common/src/posthog/helper.ts index 885db96ddaa..8a6c34bc4bd 100644 --- a/packages/common/src/posthog/helper.ts +++ b/packages/common/src/posthog/helper.ts @@ -11,10 +11,11 @@ export const COMMON_SECRET_NAMES = { type PostHogConfig = { key: string integrationName: string + integrationVersion: string } export const sendPosthogEvent = async (props: EventMessage, config: PostHogConfig): Promise => { - const { key, integrationName } = config + const { key, integrationName, integrationVersion } = config const client = new PostHog(key, { host: 'https://us.i.posthog.com', }) @@ -24,6 +25,7 @@ export const sendPosthogEvent = async (props: EventMessage, config: PostHogConfi properties: { ...props.properties, integrationName, + integrationVersion, }, } await client.captureImmediate(signedProps) @@ -85,6 +87,7 @@ function wrapFunction(fn: Function, config: PostHogConfig) { properties: { from: fn.name, integrationName: config.integrationName, + integrationVersion: config.integrationVersion, errMsg, }, }, @@ -109,6 +112,7 @@ function wrapHandler(fn: Function, config: PostHogConfig) { properties: { from: fn.name, integrationName: config.integrationName, + integrationVersion: config.integrationVersion, errMsg: 'Empty Body', }, }, @@ -123,6 +127,7 @@ function wrapHandler(fn: Function, config: PostHogConfig) { properties: { from: fn.name, integrationName: config.integrationName, + integrationVersion: config.integrationVersion, errMsg: JSON.stringify(resp.body), }, }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 7cb84748d17..c404c278a6d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/sdk", - "version": "4.20.2", + "version": "4.21.0", "description": "Botpress SDK", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/sdk/src/message.ts b/packages/sdk/src/message.ts index d576c696605..23fb62ca395 100644 --- a/packages/sdk/src/message.ts +++ b/packages/sdk/src/message.ts @@ -6,6 +6,9 @@ const textMessageSchema = z.object({ text: NonEmptyString, }) +/** + * @deprecated + */ const markdownMessageSchema = z.object({ markdown: NonEmptyString, }) @@ -61,9 +64,8 @@ const carouselSchema = z.object({ items: z.array(cardSchema), }) -const blocSchema = z.union([ +const blocItemSchema = z.union([ z.object({ type: z.literal('text'), payload: textMessageSchema }), - z.object({ type: z.literal('markdown'), payload: markdownMessageSchema }), z.object({ type: z.literal('image'), payload: imageMessageSchema }), z.object({ type: z.literal('audio'), payload: audioMessageSchema }), z.object({ type: z.literal('video'), payload: videoMessageSchema }), @@ -71,14 +73,34 @@ const blocSchema = z.union([ z.object({ type: z.literal('location'), payload: locationMessageSchema }), ]) -const blocsSchema = z.object({ - items: z.array(blocSchema), +const blocSchema = z.object({ + items: z.array(blocItemSchema), }) /** * @deprecated use `text` instead */ -export const markdown = { schema: markdownMessageSchema } +export const markdown = { + schema: markdownMessageSchema, +} + +/** + * Bloc message that still includes markdown as an item + * + * @deprecated use `bloc` instead + */ +export const markdownBloc = { + schema: z.object({ + items: z.array( + z.union([ + // + ...blocItemSchema.options, + z.object({ type: z.literal('markdown'), payload: markdownMessageSchema }), + ]) + ), + }), +} + export const defaults = { text: { schema: textMessageSchema }, image: { schema: imageMessageSchema }, @@ -90,5 +112,5 @@ export const defaults = { card: { schema: cardSchema }, dropdown: { schema: choiceSchema }, choice: { schema: choiceSchema }, - bloc: { schema: blocsSchema }, + bloc: { schema: blocSchema }, } as const // should use satisfies operator but this works for older versions of TS diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d56e89800b..081c07f8860 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1684,14 +1684,29 @@ importers: specifier: ^1.5.1 version: 1.5.1 botbuilder: - specifier: ^4.18.0 - version: 4.20.0 + specifier: ^4.23.3 + version: 4.23.3 + dedent: + specifier: ^1.6.0 + version: 1.6.0 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 jwks-rsa: specifier: ^3.1.0 version: 3.1.0 + remark: + specifier: ^15.0.1 + version: 15.0.1 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + sanitize-html: + specifier: ^2.17.0 + version: 2.17.0 + turndown: + specifier: ^7.2.2 + version: 7.2.2 devDependencies: '@botpress/cli': specifier: workspace:* @@ -1705,6 +1720,15 @@ importers: '@types/jsonwebtoken': specifier: ^9.0.3 version: 9.0.3 + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 + '@types/sanitize-html': + specifier: ^2.16.0 + version: 2.16.0 + '@types/turndown': + specifier: ^5.0.6 + version: 5.0.6 integrations/telegram: dependencies: @@ -2352,7 +2376,7 @@ importers: specifier: 1.27.2 version: link:../client '@botpress/sdk': - specifier: 4.20.2 + specifier: 4.21.0 version: link:../sdk '@bpinternal/const': specifier: ^0.1.0 @@ -2470,7 +2494,7 @@ importers: specifier: 1.27.2 version: link:../../../client '@botpress/sdk': - specifier: 4.20.2 + specifier: 4.21.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2486,7 +2510,7 @@ importers: specifier: 1.27.2 version: link:../../../client '@botpress/sdk': - specifier: 4.20.2 + specifier: 4.21.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2499,7 +2523,7 @@ importers: packages/cli/templates/empty-plugin: dependencies: '@botpress/sdk': - specifier: 4.20.2 + specifier: 4.21.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2515,7 +2539,7 @@ importers: specifier: 1.27.2 version: link:../../../client '@botpress/sdk': - specifier: 4.20.2 + specifier: 4.21.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2531,7 +2555,7 @@ importers: specifier: 1.27.2 version: link:../../../client '@botpress/sdk': - specifier: 4.20.2 + specifier: 4.21.0 version: link:../../../sdk axios: specifier: ^1.6.8 @@ -2633,8 +2657,8 @@ importers: specifier: ^6.9.0 version: 6.9.0(ws@8.18.2) posthog-node: - specifier: 5.11.2 - version: 5.11.2 + specifier: 5.14.1 + version: 5.14.1 preact: specifier: ^10.26.6 version: 10.26.6 @@ -3423,10 +3447,6 @@ packages: '@aws-sdk/util-utf8-browser@3.259.0': resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} - '@azure/abort-controller@1.1.0': - resolution: {integrity: sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==} - engines: {node: '>=12.0.0'} - '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} @@ -3439,6 +3459,10 @@ packages: resolution: {integrity: sha512-O4aP3CLFNodg8eTHXECaH3B3CjicfzkxVtnrfLkOq0XNP7TIECGfHpK/C6vADZkWP75wzmdBnsIA8ksuJMk18g==} engines: {node: '>=20.0.0'} + '@azure/core-http-compat@2.3.1': + resolution: {integrity: sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==} + engines: {node: '>=20.0.0'} + '@azure/core-rest-pipeline@1.22.0': resolution: {integrity: sha512-OKHmb3/Kpm06HypvB3g6Q3zJuvyXcpxDpCS1PnU8OV6AJgSFaee/covXBcPbWc6XDDxtEPlbi3EMQ6nUiPaQtw==} engines: {node: '>=20.0.0'} @@ -3447,14 +3471,14 @@ packages: resolution: {integrity: sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==} engines: {node: '>=12.0.0'} + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + '@azure/core-util@1.13.0': resolution: {integrity: sha512-o0psW8QWQ58fq3i24Q1K2XfS/jYTxr7O1HRcyUE9bV9NttLU+kYOH82Ixj8DGlMTOWgxm1Sss2QAfKK5UkSPxw==} engines: {node: '>=20.0.0'} - '@azure/identity@2.1.0': - resolution: {integrity: sha512-BPDz1sK7Ul9t0l9YKLEa8PHqWU4iCfhGJ+ELJl6c8CP3TpJt2urNCbm0ZHsthmxRsYoMPbz2Dvzj30zXZVmAFw==} - engines: {node: '>=12.0.0'} - '@azure/identity@4.10.2': resolution: {integrity: sha512-Uth4vz0j+fkXCkbvutChUj03PDCokjbC6Wk9JT8hHEUtpy/EurNKAseb3+gO6Zi9VYBvwt61pgbzn1ovk942Qg==} engines: {node: '>=20.0.0'} @@ -3463,34 +3487,21 @@ packages: resolution: {integrity: sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==} engines: {node: '>=14.0.0'} - '@azure/ms-rest-js@2.6.6': - resolution: {integrity: sha512-WYIda8VvrkZE68xHgOxUXvjThxNf1nnGPPe0rAljqK5HJHIZ12Pi3YhEDOn3Ge7UnwaaM3eFO0VtAy4nGVI27Q==} - - '@azure/msal-browser@2.37.1': - resolution: {integrity: sha512-EoKQISEpIY39Ru1OpWkeFZBcwp6Y0bG81bVmdyy4QJebPPDdVzfm62PSU0XFIRc3bqjZ4PBKBLMYLuo9NZYAow==} - engines: {node: '>=0.8.0'} - deprecated: A newer major version of this library is available. Please upgrade to the latest available version. - '@azure/msal-browser@4.15.0': resolution: {integrity: sha512-+AIGTvpVz+FIx5CsM1y+nW0r/qOb/ChRdM8/Cbp+jKWC0Wdw4ldnwPdYOBi5NaALUQnYITirD9XMZX7LdklEzQ==} engines: {node: '>=0.8.0'} - '@azure/msal-common@13.1.0': - resolution: {integrity: sha512-wj+ULrRB0HTuMmtrMjg8j3guCx32GE2BCPbsMCZkHgL1BZetC3o/Su5UJEQMX1HNc9CrIaQNx5WaKWHygYDe0g==} + '@azure/msal-common@14.16.1': + resolution: {integrity: sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w==} engines: {node: '>=0.8.0'} '@azure/msal-common@15.8.1': resolution: {integrity: sha512-ltIlFK5VxeJ5BurE25OsJIfcx1Q3H/IZg2LjV9d4vmH+5t4c1UCyRQ/HgKLgXuCZShs7qfc/TC95GYZfsUsJUQ==} engines: {node: '>=0.8.0'} - '@azure/msal-common@7.6.0': - resolution: {integrity: sha512-XqfbglUTVLdkHQ8F9UQJtKseRr3sSnr9ysboxtoswvaMVaEfvyLtMoHv9XdKUfOc0qKGzNgRFd9yRjIWVepl6Q==} - engines: {node: '>=0.8.0'} - - '@azure/msal-node@1.17.3': - resolution: {integrity: sha512-slsa+388bQQWnWH1V91KL+zV57rIp/0OQFfF0EmVMY8gnEIkAnpWWFUVBTTMbxEyjEFMk5ZW9xiHvHBcYFHzDw==} - engines: {node: 10 || 12 || 14 || 16 || 18} - deprecated: A newer major version of this library is available. Please upgrade to the latest available version. + '@azure/msal-node@2.16.3': + resolution: {integrity: sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==} + engines: {node: '>=16'} '@azure/msal-node@3.6.3': resolution: {integrity: sha512-95wjsKGyUcAd5tFmQBo5Ug/kOj+hFh/8FsXuxluEvdfbgg6xCimhSP9qnyq6+xIg78/jREkBD1/BSqd7NIDDYQ==} @@ -4797,6 +4808,9 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@mswjs/interceptors@0.40.0': resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} engines: {node: '>=18'} @@ -4991,8 +5005,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@posthog/core@1.5.2': - resolution: {integrity: sha512-iedUP3EnOPPxTA2VaIrsrd29lSZnUV+ZrMnvY56timRVeZAXoYCkmjfIs3KBAsF8OUT5h1GXLSkoQdrV0r31OQ==} + '@posthog/core@1.6.0': + resolution: {integrity: sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==} '@react-email/body@0.0.10': resolution: {integrity: sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==} @@ -5938,6 +5952,9 @@ packages: '@types/jsonwebtoken@9.0.3': resolution: {integrity: sha512-b0jGiOgHtZ2jqdPgPnP6WLCXZk1T8p06A/vPGzUvxpFGgKMbjXJDjC5m52ErqBnIuWZFgGoIJyRdeG5AyreJjA==} + '@types/jsonwebtoken@9.0.6': + resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -6049,6 +6066,9 @@ packages: '@types/triple-beam@1.3.2': resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==} + '@types/turndown@5.0.6': + resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -6212,11 +6232,6 @@ packages: '@vitest/utils@2.1.8': resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} - '@xmldom/xmldom@0.7.11': - resolution: {integrity: sha512-UDi3g6Jss/W5FnSzO9jCtQwEpfymt0M+sPPlmLhDH6h2TJ8j4ESE/LpmNPBij15J5NKkk4/cg/qoVMdWI3vnlQ==} - engines: {node: '>=10.0.0'} - deprecated: this version is no longer supported, please update to at least 0.8.* - abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -6246,10 +6261,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - adal-node@0.2.3: - resolution: {integrity: sha512-gMKr8RuYEYvsj7jyfCv/4BfKToQThz20SP71N3AtFn3ia3yAR8Qt2T3aVQhuJzunWs2b38ZsQV0qsZPdwZr7VQ==} - engines: {node: '>= 0.6.15'} - deprecated: This package is no longer supported. Please migrate to @azure/msal-node. + adaptivecards@1.2.3: + resolution: {integrity: sha512-amQ5OSW3OpIkrxVKLjxVBPk/T49yuOtnqs1z5ZPfZr0+OpTovzmiHbyoAGDIsu5SNYHwOZFp/3LGOnRaALFa/g==} agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} @@ -6422,9 +6435,6 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - async@2.6.4: - resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} - async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} @@ -6468,9 +6478,6 @@ packages: axios@0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} - axios@0.25.0: - resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==} - axios@0.26.1: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} @@ -6602,26 +6609,26 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - botbuilder-core@4.20.0: - resolution: {integrity: sha512-UxJF31nkIuiVHerPhtJKAyzfIbdG7sTgsS4bXvCqkQvxaY+60p6mIwuxOZZQf3AIOPIxCysMKAmhfoaFyTc+Uw==} + botbuilder-core@4.23.3: + resolution: {integrity: sha512-48iW739I24piBH683b/Unvlu1fSzjB69ViOwZ0PbTkN2yW5cTvHJWlW7bXntO8GSqJfssgPaVthKfyaCW457ig==} - botbuilder-dialogs-adaptive-runtime-core@4.20.0-preview: - resolution: {integrity: sha512-P7ezlaFsv5xPHGRYHHsb5UgvkbyxCj0OTHpIfIRCPYLWaKYrzcLI46zzIj76XImn/aYLUsKU7Xg/qw13l9sPKA==} + botbuilder-dialogs-adaptive-runtime-core@4.23.3-preview: + resolution: {integrity: sha512-EssyvqK9MobX3gbnUe/jjhLuxpCEeyQeQsyUFMJ236U6vzSQdrAxNH7Jc5DyZw2KKelBdK1xPBdwTYQNM5S0Qw==} - botbuilder-stdlib@4.20.0-internal: - resolution: {integrity: sha512-WtMQkl1PHWX+GkdqufDC4nv+JZTUitvjLpdh56piQaakxozK6FQqQzJFdMvUdOMgfJ/mQMPmtojLhfbQOKYvfA==} + botbuilder-stdlib@4.23.3-internal: + resolution: {integrity: sha512-fwvIHnKU8sXo1gTww+m/k8wnuM5ktVBAV/3vWJ+ou40zapy1HYjWQuu6sVCRFgMUngpKwhdmoOQsTXsp58SNtA==} - botbuilder@4.20.0: - resolution: {integrity: sha512-YfJgAcUyjKZQP3XzXqBoQmj8S5NoIGmqX5g/5coLlsNEaFLAbQXmOEBddN+ww4gz49S246MDspoGaqtweTu/pw==} + botbuilder@4.23.3: + resolution: {integrity: sha512-1gDIQHHYosYBHGXMjvZEJDrcp3NGy3lzHBml5wn9PFqVuIk/cbsCDZs3KJ3g+aH/GGh4CH/ij9iQ2KbQYHAYKA==} - botframework-connector@4.20.0: - resolution: {integrity: sha512-3mP67NHOGdLeODxuXNchK9gzzTafzLdBGZDSWkJDRvIPORbfoxvA/kXsWU2USwMXBnu/M5YeDZn/eUPjDu1nvw==} + botframework-connector@4.23.3: + resolution: {integrity: sha512-sChwCFJr3xhcMCYChaOxJoE8/YgdjOPWzGwz5JAxZDwhbQonwYX5O/6Z9EA+wB3TCFNEh642SGeC/rOitaTnGQ==} - botframework-schema@4.20.0: - resolution: {integrity: sha512-Tda488691XFlkBKdMLdlGWRI8IebLprxqQf57LpuRQHqK2ttbvmfwjFiW5V3VcTBBz1SVzMhwJBAWVDG+MexLA==} + botframework-schema@4.23.3: + resolution: {integrity: sha512-/W0uWxZ3fuPLAImZRLnPTbs49Z2xMpJSIzIBxSfvwO0aqv9GsM3bTk3zlNdJ1xr40SshQ7WiH2H1hgjBALwYJw==} - botframework-streaming@4.20.0: - resolution: {integrity: sha512-yPH9+BYJ9RPb76OcARjls3QHfwRejNQz9RxR9YXt6OX0nMfP+sdMfE8BYTDqvBiIXLivbPi+pJG334PwskfohA==} + botframework-streaming@4.23.3: + resolution: {integrity: sha512-GMtciQGfZXtAW6syUqFpFJQ2vDyVbpxL3T1DqFzq/GmmkAu7KTZ1zvo7PTww6+IT1kMW0lmL/XZJVq3Rhg4PQA==} bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -6691,6 +6698,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -6980,8 +6990,8 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-fetch@3.1.6: - resolution: {integrity: sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==} + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -7024,9 +7034,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - date-utils@1.2.21: - resolution: {integrity: sha512-wJMBjqlwXR0Iv0wUo/lFbhSQ7MmG1hl36iuxuE91kW+5b5sWbase73manEqNH9sOLFAMG83B4ffNKq9/Iq0FVA==} - engines: {node: '>0.4.0'} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} dayjs@1.11.8: resolution: {integrity: sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==} @@ -7141,10 +7150,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -7165,14 +7170,14 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - dependency-graph@0.10.0: - resolution: {integrity: sha512-c9amUgpgxSi1bE5/sbLwcs5diLD0ygCQYmhfM5H1s5VH1mCsYkcmAL3CcNdv4kdSw6JuMoHeDGzLgj/gAXdWVg==} - engines: {node: '>= 0.6.0'} - dependency-graph@0.11.0: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} + dependency-graph@1.0.0: + resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} + engines: {node: '>=4'} + deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} @@ -7218,29 +7223,16 @@ packages: resolution: {integrity: sha512-vLeDSDfCRUtQG7u50HSKarz8bmBqAfDPJXgi5hjdwB/43tYtTEt/lWiHivxYOKNqxwWrAoBGfAXGlflkEnUOrg==} engines: {node: '>=2.2.1'} - dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} - domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - - domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -7321,9 +7313,6 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -7433,10 +7422,6 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -7589,10 +7574,6 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - eventsource@2.0.2: resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} engines: {node: '>=12.0.0'} @@ -7720,13 +7701,13 @@ packages: resolution: {integrity: sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==} engines: {node: '>=4'} - filename-reserved-regex@2.0.0: - resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} - engines: {node: '>=4'} + filename-reserved-regex@3.0.0: + resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - filenamify@4.3.0: - resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} - engines: {node: '>=8'} + filenamify@6.0.0: + resolution: {integrity: sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==} + engines: {node: '>=16'} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -7853,6 +7834,10 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -8164,12 +8149,12 @@ packages: htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} - htmlparser2@6.1.0: - resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} - htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -8278,10 +8263,6 @@ packages: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} - ip-regex@2.1.0: - resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==} - engines: {node: '>=4'} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -8336,11 +8317,6 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8482,10 +8458,6 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -8782,6 +8754,9 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonpath-plus@6.0.1: resolution: {integrity: sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==} engines: {node: '>=10.0.0'} @@ -9502,10 +9477,6 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - openai@5.12.1: resolution: {integrity: sha512-26s536j4Fi7P3iUma1S9H33WRrw0Qu8pJ2nYJHffrlKHPU0JK4d0r3NcMgqEcAeTdNLGYNyoFsqN4g4YE9vutg==} hasBin: true @@ -9540,6 +9511,9 @@ packages: openapi3-ts@2.0.2: resolution: {integrity: sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==} + openssl-wrapper@0.3.4: + resolution: {integrity: sha512-iITsrx6Ho8V3/2OVtmZzzX8wQaKAaFXEJQdzoPUZDtyf5jWFlqo+h+OhGT4TATQ47f9ACKHua8nw7Qoy85aeKQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -9834,8 +9808,8 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - posthog-node@5.11.2: - resolution: {integrity: sha512-z+XekcBUmGePMsjPlGaEF2bJFiDHKHYPQjS4OEw4YPDQz8s7Owuim/L7xNX+6UJkyIRniBza9iC7bW8yrGTv1g==} + posthog-node@5.14.1: + resolution: {integrity: sha512-NbqZMCwHectzfeFOeLqes1fPg/V5bsKhrBfyH1qHEcDb4ZHcbDARcqLE6JDhwMDQKa4YHkInXHITYscMuPylFw==} engines: {node: '>=20'} preact-render-to-string@6.5.13: @@ -10140,8 +10114,8 @@ packages: rootpath@0.1.2: resolution: {integrity: sha512-R3wLbuAYejpxQjL/SjXo1Cjv4wcJECnMRT/FlcCfTwCBhaji9rWaRCoVEQ1SPiTJ4kKK+yh+bZLAV7SCafoDDw==} - rsa-pem-from-mod-exp@0.8.5: - resolution: {integrity: sha512-D5dt0kd9zpOyZJNk3ObG/wJQCfwDwSD1DawIkRr7LXcflcuvWXqhU0QTFkuJNXM8KZJaXw6TD6xCA2SDHqpZzg==} + rsa-pem-from-mod-exp@0.8.6: + resolution: {integrity: sha512-c5ouQkOvGHF1qomUUDJGFcXsomeSO2gbEs6hVhMAtlkE1CuaZase/WzoaKFG/EZQuNmq6pw/EMCeEnDvOgCJYQ==} run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} @@ -10191,9 +10165,6 @@ packages: sanitize-html@2.17.0: resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} - sax@1.2.4: - resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} - scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -10441,10 +10412,6 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stoppable@1.1.0: - resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} - engines: {node: '>=4', npm: '>=6'} - strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -10531,10 +10498,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-outer@1.0.1: - resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} - engines: {node: '>=0.10.0'} - stripe@13.11.0: resolution: {integrity: sha512-yPxVJxUzP1QHhHeFnYjJl48QwDS1+5befcL7ju7+t+i88D5r0rbsL+GkCCS6zgcU+TiV5bF9eMGcKyJfLf8BZQ==} engines: {node: '>=12.*'} @@ -10678,10 +10641,6 @@ packages: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} - tough-cookie@3.0.1: - resolution: {integrity: sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==} - engines: {node: '>=6'} - tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -10702,10 +10661,6 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - trim-repeated@1.0.0: - resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} - engines: {node: '>=0.10.0'} - triple-beam@1.3.0: resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==} @@ -10814,10 +10769,6 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - tunnel@0.0.6: - resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} - engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - turbo-darwin-64@2.3.3: resolution: {integrity: sha512-bxX82xe6du/3rPmm4aCC5RdEilIN99VUld4HkFQuw+mvFg6darNBuQxyWSHZTtc25XgYjQrjsV05888w1grpaA==} cpu: [x64] @@ -10852,6 +10803,9 @@ packages: resolution: {integrity: sha512-DUHWQAcC8BTiUZDRzAYGvpSpGLiaOQPfYXlCieQbwUvmml/LRGIe3raKdrOPOoiX0DYlzxs2nH6BoWJoZrj8hA==} hasBin: true + turndown@7.2.2: + resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==} + tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} @@ -10953,9 +10907,6 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - underscore@1.13.6: - resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -10998,6 +10949,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -11302,8 +11257,8 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@7.5.9: - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 @@ -11342,22 +11297,10 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} - xml2js@0.5.0: - resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} - engines: {node: '>=4.0.0'} - - xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - xmlbuilder@13.0.2: resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==} engines: {node: '>=6.0'} - xpath.js@1.1.0: - resolution: {integrity: sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==} - engines: {node: '>=0.4.0'} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -11425,9 +11368,6 @@ packages: peerDependencies: zod: ^3.24.1 - zod@1.11.17: - resolution: {integrity: sha512-UzIwO92D0dSFwIRyyqAfRXICITLjF0IP8tRbEK/un7adirMssWZx8xF/1hZNE7t61knWZ+lhEuUvxlu2MO8qqA==} - zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -12267,10 +12207,6 @@ snapshots: dependencies: tslib: 2.6.2 - '@azure/abort-controller@1.1.0': - dependencies: - tslib: 2.6.2 - '@azure/abort-controller@2.1.2': dependencies: tslib: 2.6.2 @@ -12295,6 +12231,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@azure/core-http-compat@2.3.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.0 + '@azure/core-rest-pipeline': 1.22.0 + transitivePeerDependencies: + - supports-color + '@azure/core-rest-pipeline@1.22.0': dependencies: '@azure/abort-controller': 2.1.2 @@ -12311,32 +12255,15 @@ snapshots: dependencies: tslib: 2.6.2 - '@azure/core-util@1.13.0': + '@azure/core-tracing@1.3.1': dependencies: - '@azure/abort-controller': 2.1.2 - '@typespec/ts-http-runtime': 0.3.0 tslib: 2.6.2 - transitivePeerDependencies: - - supports-color - '@azure/identity@2.1.0': + '@azure/core-util@1.13.0': dependencies: - '@azure/abort-controller': 1.1.0 - '@azure/core-auth': 1.10.0 - '@azure/core-client': 1.10.0 - '@azure/core-rest-pipeline': 1.22.0 - '@azure/core-tracing': 1.0.1 - '@azure/core-util': 1.13.0 - '@azure/logger': 1.0.4 - '@azure/msal-browser': 2.37.1 - '@azure/msal-common': 7.6.0 - '@azure/msal-node': 1.17.3 - events: 3.3.0 - jws: 4.0.0 - open: 8.4.2 - stoppable: 1.1.0 + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.0 tslib: 2.6.2 - uuid: 8.3.2 transitivePeerDependencies: - supports-color @@ -12360,38 +12287,17 @@ snapshots: dependencies: tslib: 2.6.2 - '@azure/ms-rest-js@2.6.6': - dependencies: - '@azure/core-auth': 1.10.0 - abort-controller: 3.0.0 - form-data: 2.5.1 - node-fetch: 2.7.0 - tough-cookie: 3.0.1 - tslib: 1.14.1 - tunnel: 0.0.6 - uuid: 8.3.2 - xml2js: 0.5.0 - transitivePeerDependencies: - - encoding - - supports-color - - '@azure/msal-browser@2.37.1': - dependencies: - '@azure/msal-common': 13.1.0 - '@azure/msal-browser@4.15.0': dependencies: '@azure/msal-common': 15.8.1 - '@azure/msal-common@13.1.0': {} + '@azure/msal-common@14.16.1': {} '@azure/msal-common@15.8.1': {} - '@azure/msal-common@7.6.0': {} - - '@azure/msal-node@1.17.3': + '@azure/msal-node@2.16.3': dependencies: - '@azure/msal-common': 13.1.0 + '@azure/msal-common': 14.16.1 jsonwebtoken: 9.0.2 uuid: 8.3.2 @@ -13746,6 +13652,8 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@mixmark-io/domino@2.2.0': {} + '@mswjs/interceptors@0.40.0': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -14002,7 +13910,7 @@ snapshots: '@pkgr/core@0.2.9': {} - '@posthog/core@1.5.2': + '@posthog/core@1.6.0': dependencies: cross-spawn: 7.0.6 @@ -15246,6 +15154,10 @@ snapshots: dependencies: '@types/node': 22.16.4 + '@types/jsonwebtoken@9.0.6': + dependencies: + '@types/node': 22.16.4 + '@types/keyv@3.1.4': dependencies: '@types/node': 22.16.4 @@ -15354,6 +15266,8 @@ snapshots: '@types/triple-beam@1.3.2': {} + '@types/turndown@5.0.6': {} + '@types/unist@3.0.3': {} '@types/urijs@1.19.25': {} @@ -15575,8 +15489,6 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 - '@xmldom/xmldom@0.7.11': {} - abbrev@2.0.0: {} abort-controller@3.0.0: @@ -15598,18 +15510,7 @@ snapshots: acorn@8.15.0: {} - adal-node@0.2.3: - dependencies: - '@xmldom/xmldom': 0.7.11 - async: 2.6.4 - axios: 0.21.4(debug@4.4.1) - date-utils: 1.2.21 - jws: 3.2.2 - underscore: 1.13.6 - uuid: 3.4.0 - xpath.js: 1.1.0 - transitivePeerDependencies: - - debug + adaptivecards@1.2.3: {} agent-base@6.0.2: dependencies: @@ -15805,10 +15706,6 @@ snapshots: astring@1.9.0: {} - async@2.6.4: - dependencies: - lodash: 4.17.21 - async@3.2.4: {} asynckit@0.4.0: {} @@ -15865,12 +15762,6 @@ snapshots: transitivePeerDependencies: - debug - axios@0.25.0: - dependencies: - follow-redirects: 1.15.6(debug@4.4.1) - transitivePeerDependencies: - - debug - axios@0.26.1: dependencies: follow-redirects: 1.15.6(debug@4.4.1) @@ -15902,7 +15793,7 @@ snapshots: axios@1.13.1: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6 form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16128,40 +16019,50 @@ snapshots: boolbase@1.0.0: {} - botbuilder-core@4.20.0: + botbuilder-core@4.23.3: dependencies: - botbuilder-dialogs-adaptive-runtime-core: 4.20.0-preview - botbuilder-stdlib: 4.20.0-internal - botframework-connector: 4.20.0 - botframework-schema: 4.20.0 - uuid: 8.3.2 - zod: 1.11.17 + botbuilder-dialogs-adaptive-runtime-core: 4.23.3-preview + botbuilder-stdlib: 4.23.3-internal + botframework-connector: 4.23.3 + botframework-schema: 4.23.3 + uuid: 10.0.0 + zod: 3.24.2 transitivePeerDependencies: - debug - encoding - supports-color - botbuilder-dialogs-adaptive-runtime-core@4.20.0-preview: + botbuilder-dialogs-adaptive-runtime-core@4.23.3-preview: dependencies: - dependency-graph: 0.10.0 + dependency-graph: 1.0.0 - botbuilder-stdlib@4.20.0-internal: {} + botbuilder-stdlib@4.23.3-internal: + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.0 + '@azure/core-client': 1.10.0 + '@azure/core-http-compat': 2.3.1 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-tracing': 1.3.1 + transitivePeerDependencies: + - supports-color - botbuilder@4.20.0: + botbuilder@4.23.3: dependencies: - '@azure/ms-rest-js': 2.6.6 - axios: 0.25.0 - botbuilder-core: 4.20.0 - botbuilder-stdlib: 4.20.0-internal - botframework-connector: 4.20.0 - botframework-schema: 4.20.0 - botframework-streaming: 4.20.0 - dayjs: 1.11.8 - filenamify: 4.3.0 - fs-extra: 7.0.1 - htmlparser2: 6.1.0 - uuid: 8.3.2 - zod: 1.11.17 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/msal-node': 2.16.3 + axios: 1.13.1 + botbuilder-core: 4.23.3 + botbuilder-stdlib: 4.23.3-internal + botframework-connector: 4.23.3 + botframework-schema: 4.23.3 + botframework-streaming: 4.23.3 + dayjs: 1.11.19 + filenamify: 6.0.0 + fs-extra: 11.3.2 + htmlparser2: 9.1.0 + uuid: 10.0.0 + zod: 3.24.2 transitivePeerDependencies: - bufferutil - debug @@ -16169,35 +16070,40 @@ snapshots: - supports-color - utf-8-validate - botframework-connector@4.20.0: + botframework-connector@4.23.3: dependencies: - '@azure/identity': 2.1.0 - '@azure/ms-rest-js': 2.6.6 - adal-node: 0.2.3 - axios: 0.25.0 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/identity': 4.10.2 + '@azure/msal-node': 2.16.3 + '@types/jsonwebtoken': 9.0.6 + axios: 1.13.1 base64url: 3.0.1 - botbuilder-stdlib: 4.20.0-internal - botframework-schema: 4.20.0 - cross-fetch: 3.1.6 + botbuilder-stdlib: 4.23.3-internal + botframework-schema: 4.23.3 + buffer: 6.0.3 + cross-fetch: 4.1.0 + https-proxy-agent: 7.0.6 jsonwebtoken: 9.0.2 - rsa-pem-from-mod-exp: 0.8.5 - zod: 1.11.17 + node-fetch: 2.7.0 + openssl-wrapper: 0.3.4 + rsa-pem-from-mod-exp: 0.8.6 + zod: 3.24.2 transitivePeerDependencies: - debug - encoding - supports-color - botframework-schema@4.20.0: + botframework-schema@4.23.3: dependencies: - uuid: 8.3.2 - zod: 1.11.17 + adaptivecards: 1.2.3 + uuid: 10.0.0 + zod: 3.24.2 - botframework-streaming@4.20.0: + botframework-streaming@4.23.3: dependencies: - '@types/node': 10.17.60 '@types/ws': 6.0.4 - uuid: 8.3.2 - ws: 7.5.9 + uuid: 10.0.0 + ws: 7.5.10 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -16278,6 +16184,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 @@ -16569,7 +16480,7 @@ snapshots: create-require@1.1.1: {} - cross-fetch@3.1.6: + cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 transitivePeerDependencies: @@ -16586,7 +16497,7 @@ snapshots: boolbase: 1.0.0 css-what: 6.1.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 nth-check: 2.1.1 css-what@6.1.0: {} @@ -16626,7 +16537,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - date-utils@1.2.21: {} + dayjs@1.11.19: {} dayjs@1.11.8: {} @@ -16723,8 +16634,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - define-lazy-prop@2.0.0: {} - define-lazy-prop@3.0.0: {} define-properties@1.2.1: @@ -16739,10 +16648,10 @@ snapshots: depd@2.0.0: {} - dependency-graph@0.10.0: {} - dependency-graph@0.11.0: {} + dependency-graph@1.0.0: {} + deprecation@2.3.1: {} dequal@2.0.3: {} @@ -16785,12 +16694,6 @@ snapshots: transitivePeerDependencies: - debug - dom-serializer@1.4.1: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - entities: 2.2.0 - dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -16799,26 +16702,10 @@ snapshots: domelementtype@2.3.0: {} - domhandler@4.3.1: - dependencies: - domelementtype: 2.3.0 - domhandler@5.0.3: dependencies: domelementtype: 2.3.0 - domutils@2.8.0: - dependencies: - dom-serializer: 1.4.1 - domelementtype: 2.3.0 - domhandler: 4.3.1 - - domutils@3.1.0: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -16896,8 +16783,6 @@ snapshots: dependencies: once: 1.4.0 - entities@2.2.0: {} - entities@4.5.0: {} entities@6.0.1: {} @@ -17210,8 +17095,6 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} - escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} @@ -17397,8 +17280,6 @@ snapshots: eventemitter3@5.0.1: {} - events@3.3.0: {} - eventsource@2.0.2: {} execa@5.1.1: @@ -17554,13 +17435,11 @@ snapshots: file-type@6.2.0: {} - filename-reserved-regex@2.0.0: {} + filename-reserved-regex@3.0.0: {} - filenamify@4.3.0: + filenamify@6.0.0: dependencies: - filename-reserved-regex: 2.0.0 - strip-outer: 1.0.1 - trim-repeated: 1.0.0 + filename-reserved-regex: 3.0.0 fill-range@7.1.1: dependencies: @@ -17611,6 +17490,8 @@ snapshots: follow-redirects@1.15.5: {} + follow-redirects@1.15.6: {} + follow-redirects@1.15.6(debug@4.4.1): optionalDependencies: debug: 4.4.1 @@ -17690,6 +17571,12 @@ snapshots: fs-constants@1.0.0: {} + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -18113,18 +18000,18 @@ snapshots: domutils: 3.2.2 entities: 6.0.1 - htmlparser2@6.1.0: + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 - domhandler: 4.3.1 - domutils: 2.8.0 - entities: 2.2.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 - htmlparser2@8.0.2: + htmlparser2@9.1.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 entities: 4.5.0 http-cache-semantics@4.1.1: {} @@ -18246,8 +18133,6 @@ snapshots: transitivePeerDependencies: - supports-color - ip-regex@2.1.0: {} - ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: @@ -18305,8 +18190,6 @@ snapshots: call-bound: 1.0.3 has-tostringtag: 1.0.2 - is-docker@2.2.1: {} - is-docker@3.0.0: {} is-electron@2.2.0: {} @@ -18420,10 +18303,6 @@ snapshots: call-bound: 1.0.3 get-intrinsic: 1.3.0 - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -18910,6 +18789,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jsonpath-plus@6.0.1: optional: true @@ -19869,12 +19754,6 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - open@8.4.2: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - openai@5.12.1(ws@8.18.2)(zod@3.24.2): optionalDependencies: ws: 8.18.2 @@ -19899,6 +19778,8 @@ snapshots: dependencies: yaml: 1.10.2 + openssl-wrapper@0.3.4: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -20161,9 +20042,9 @@ snapshots: dependencies: xtend: 4.0.2 - posthog-node@5.11.2: + posthog-node@5.14.1: dependencies: - '@posthog/core': 1.5.2 + '@posthog/core': 1.6.0 preact-render-to-string@6.5.13(preact@10.26.6): dependencies: @@ -20523,7 +20404,7 @@ snapshots: rootpath@0.1.2: {} - rsa-pem-from-mod-exp@0.8.5: {} + rsa-pem-from-mod-exp@0.8.6: {} run-applescript@7.0.0: {} @@ -20577,8 +20458,6 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.4.47 - sax@1.2.4: {} - scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -20842,8 +20721,6 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stoppable@1.1.0: {} - strict-event-emitter@0.5.1: {} strict-uri-encode@2.0.0: {} @@ -20935,10 +20812,6 @@ snapshots: strip-json-comments@3.1.1: {} - strip-outer@1.0.1: - dependencies: - escape-string-regexp: 1.0.5 - stripe@13.11.0: dependencies: '@types/node': 22.16.4 @@ -21137,12 +21010,6 @@ snapshots: psl: 1.9.0 punycode: 2.3.1 - tough-cookie@3.0.1: - dependencies: - ip-regex: 2.1.0 - psl: 1.9.0 - punycode: 2.3.1 - tough-cookie@6.0.0: dependencies: tldts: 7.0.17 @@ -21165,10 +21032,6 @@ snapshots: trim-lines@3.0.1: {} - trim-repeated@1.0.0: - dependencies: - escape-string-regexp: 1.0.5 - triple-beam@1.3.0: {} trough@2.2.0: {} @@ -21296,8 +21159,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - tunnel@0.0.6: {} - turbo-darwin-64@2.3.3: optional: true @@ -21325,6 +21186,10 @@ snapshots: turbo-windows-64: 2.3.3 turbo-windows-arm64: 2.3.3 + turndown@7.2.2: + dependencies: + '@mixmark-io/domino': 2.2.0 + tweetnacl@0.14.5: {} twilio@3.84.1: @@ -21438,8 +21303,6 @@ snapshots: buffer: 5.7.1 through: 2.3.8 - underscore@1.13.6: {} - undici-types@6.21.0: {} undici@5.28.4: @@ -21492,6 +21355,8 @@ snapshots: universalify@0.1.2: {} + universalify@2.0.1: {} + unpipe@1.0.0: {} until-async@3.0.2: {} @@ -21850,7 +21715,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@7.5.9: {} + ws@7.5.10: {} ws@8.13.0: {} @@ -21860,17 +21725,8 @@ snapshots: dependencies: is-wsl: 3.1.0 - xml2js@0.5.0: - dependencies: - sax: 1.2.4 - xmlbuilder: 11.0.1 - - xmlbuilder@11.0.1: {} - xmlbuilder@13.0.2: {} - xpath.js@1.1.0: {} - xtend@4.0.2: {} y18n@5.0.8: {} @@ -21918,8 +21774,6 @@ snapshots: dependencies: zod: 3.24.2 - zod@1.11.17: {} - zod@3.22.4: {} zod@3.23.8: {}