From b16129a5d87fb9a66f0a5dec0dac2085cb179441 Mon Sep 17 00:00:00 2001 From: Sergei Kofman <52934469+sergeikofman444@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:50:46 -0500 Subject: [PATCH 1/2] feat(zendesk): add optional "ticket form id" field to zendesk start HITL card and zendesk create ticket card (#14995) --- integrations/zendesk/integration.definition.ts | 10 +++++++++- integrations/zendesk/src/actions/create-ticket.ts | 13 +++++++++---- integrations/zendesk/src/actions/hitl.ts | 1 + integrations/zendesk/src/definitions/actions.ts | 6 ++++++ integrations/zendesk/src/definitions/schemas.ts | 12 ++++++++++-- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/integrations/zendesk/integration.definition.ts b/integrations/zendesk/integration.definition.ts index 899bcb9ac5e..fb63c93ed86 100644 --- a/integrations/zendesk/integration.definition.ts +++ b/integrations/zendesk/integration.definition.ts @@ -6,7 +6,7 @@ import { actions, events, configuration, channels, states, user } from './src/de export default new sdk.IntegrationDefinition({ name: 'zendesk', title: 'Zendesk', - version: '3.0.8', + version: '3.1.0', icon: 'icon.svg', description: 'Optimize your support workflow. Trigger workflows from ticket updates as well as manage tickets, access conversations, and engage with customers.', @@ -61,6 +61,14 @@ export default new sdk.IntegrationDefinition({ '⚠️This needs a requester name to work. The email of the requester the bot was talking to. This will be set in zendesk.' ) .optional(), + ticketFormId: sdk.z + .string() + .regex(/^\d+$/, 'Must be a numeric ID') + .title('Ticket Form ID') + .describe( + 'The ID of the ticket form to use when creating the ticket. This needs to be set up in Zendesk beforehand.' + ) + .optional(), }), }, }, diff --git a/integrations/zendesk/src/actions/create-ticket.ts b/integrations/zendesk/src/actions/create-ticket.ts index 203281bab29..37e210d57f6 100644 --- a/integrations/zendesk/src/actions/create-ticket.ts +++ b/integrations/zendesk/src/actions/create-ticket.ts @@ -5,10 +5,15 @@ import * as bp from '.botpress' export const createTicket: bp.IntegrationProps['actions']['createTicket'] = async (props) => { const { client: bpClient, ctx, input, logger } = props const zendeskClient = await getZendeskClient(bpClient, ctx, logger) - const ticket = await zendeskClient.createTicket(input.subject, input.comment, { - name: input.requesterName, - email: input.requesterEmail, - }) + const ticket = await zendeskClient.createTicket( + input.subject, + input.comment, + { + name: input.requesterName, + email: input.requesterEmail, + }, + input.ticketFormId ? { ticket_form_id: parseInt(input.ticketFormId, 10) } : {} + ) return { ticket: transformTicket(ticket), diff --git a/integrations/zendesk/src/actions/hitl.ts b/integrations/zendesk/src/actions/hitl.ts index cec5b990694..0c009797fb5 100644 --- a/integrations/zendesk/src/actions/hitl.ts +++ b/integrations/zendesk/src/actions/hitl.ts @@ -43,6 +43,7 @@ export const startHitl: bp.IntegrationProps['actions']['startHitl'] = async (pro requester, { priority: input.hitlSession?.priority, + ...(input.hitlSession?.ticketFormId ? { ticket_form_id: parseInt(input.hitlSession.ticketFormId, 10) } : {}), } ) diff --git a/integrations/zendesk/src/definitions/actions.ts b/integrations/zendesk/src/definitions/actions.ts index 80756bc51e5..23f7051ef8b 100644 --- a/integrations/zendesk/src/definitions/actions.ts +++ b/integrations/zendesk/src/definitions/actions.ts @@ -10,6 +10,12 @@ const createTicket = { comment: z.string().title('Ticket Comment').describe('Comment for the ticket'), requesterName: z.string().title('Requester Name').describe('Requester name'), requesterEmail: z.string().title('Requester Email').describe('Requester email'), + ticketFormId: z + .string() + .regex(/^\d+$/, 'Must be a numeric ID') + .title('Ticket Form ID') + .describe('Ticket Form ID') + .optional(), }), }, output: { diff --git a/integrations/zendesk/src/definitions/schemas.ts b/integrations/zendesk/src/definitions/schemas.ts index 78dd7bbf67b..4687e85b0fe 100644 --- a/integrations/zendesk/src/definitions/schemas.ts +++ b/integrations/zendesk/src/definitions/schemas.ts @@ -14,6 +14,12 @@ export const ticketSchema = z.object({ priority: z.enum(['low', 'normal', 'high', 'urgent']).nullable().title('Priority').describe('Ticket priority'), requesterId: z.number().title('Requester ID').describe('ID of the requester'), requester: requesterSchema.optional().title('Requester').describe('Requester information'), + ticketFormId: z + .string() + .nullable() + .optional() + .title('Ticket Form ID') + .describe('ID of the ticket form used when creating the ticket'), assigneeId: z.number().nullable().title('Assignee ID').describe('ID of the assignee'), createdAt: z.string().title('Created At').describe('Ticket creation date'), updatedAt: z.string().title('Updated At').describe('Ticket last update date'), @@ -28,12 +34,13 @@ export const ticketSchema = z.object({ }) const _zdTicketSchema = ticketSchema.transform((data) => ({ - ...omit(data, ['requesterId', 'assigneeId', 'createdAt', 'updatedAt', 'externalId']), + ...omit(data, ['requesterId', 'assigneeId', 'createdAt', 'updatedAt', 'externalId', 'ticketFormId']), created_at: data.createdAt, updated_at: data.updatedAt, requester_id: data.requesterId, assignee_id: data.assigneeId, external_id: data.externalId, + ticket_form_id: data.ticketFormId != null ? parseInt(data.ticketFormId, 10) : data.ticketFormId, })) export type ZendeskTicket = z.output @@ -41,12 +48,13 @@ export type Ticket = z.input export const transformTicket = (ticket: ZendeskTicket): Ticket => { return { - ...omit(ticket, ['requester_id', 'assignee_id', 'created_at', 'updated_at', 'external_id']), + ...omit(ticket, ['requester_id', 'assignee_id', 'created_at', 'updated_at', 'external_id', 'ticket_form_id']), requesterId: ticket.requester_id, assigneeId: ticket.assignee_id, createdAt: ticket.created_at, updatedAt: ticket.updated_at, externalId: ticket.external_id, + ticketFormId: ticket.ticket_form_id != null ? String(ticket.ticket_form_id) : null, } } From 6a6bd19f218c8171cd0b5370d711eb1b4c9d5789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Gagn=C3=A9=20Cliche?= <48337098+FelixGCliche@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:19:43 -0500 Subject: [PATCH 2/2] feat(slack): Add new "Create from manifest" configuration option (#14993) --- .../slack/definitions/configuration.ts | 10 + integrations/slack/definitions/states.ts | 37 +++ integrations/slack/hub.md | 23 ++ integrations/slack/integration.definition.ts | 2 +- integrations/slack/manifestHandler.vrl | 4 + integrations/slack/package.json | 4 +- integrations/slack/src/oauth-wizard/index.ts | 24 ++ integrations/slack/src/oauth-wizard/wizard.ts | 165 +++++++++ integrations/slack/src/setup.ts | 18 +- .../slack/src/slack-api/card-renderer.ts | 12 +- .../slack/src/slack-api/error-handling.ts | 23 ++ integrations/slack/src/slack-api/index.ts | 1 + .../slack/src/slack-api/slack-client.ts | 129 +++++--- .../src/slack-api/slack-manifest-client.ts | 275 +++++++++++++++ .../slack/src/slack-api/slack-oauth-client.ts | 27 +- .../src/webhook-events/handler-dispatcher.ts | 23 +- .../handlers/message-received.ts | 2 +- .../webhook-events/handlers/oauth-callback.ts | 3 +- integrations/slack/tsconfig.json | 4 +- pnpm-lock.yaml | 312 ++++++++++++------ 20 files changed, 928 insertions(+), 170 deletions(-) create mode 100644 integrations/slack/manifestHandler.vrl create mode 100644 integrations/slack/src/oauth-wizard/index.ts create mode 100644 integrations/slack/src/oauth-wizard/wizard.ts create mode 100644 integrations/slack/src/slack-api/slack-manifest-client.ts diff --git a/integrations/slack/definitions/configuration.ts b/integrations/slack/definitions/configuration.ts index 1a296adbdbf..34a979542da 100644 --- a/integrations/slack/definitions/configuration.ts +++ b/integrations/slack/definitions/configuration.ts @@ -66,4 +66,14 @@ export const configurations = { ...SHARED_CONFIGURATION, }), }, + manifestAppCredentials: { + title: 'App Manifest (Automatic Setup)', + description: 'Register new Slack application', + identifier: { + linkTemplateScript: 'manifestHandler.vrl', + }, + schema: sdk.z.object({ + ...SHARED_CONFIGURATION, + }), + }, } as const satisfies sdk.IntegrationDefinitionProps['configurations'] diff --git a/integrations/slack/definitions/states.ts b/integrations/slack/definitions/states.ts index 48c7debf617..fcb2095e2a8 100644 --- a/integrations/slack/definitions/states.ts +++ b/integrations/slack/definitions/states.ts @@ -50,4 +50,41 @@ export const states = { .describe('The now-revoked refresh token that was used to set up the integration'), }), }, + manifestAppCredentials: { + type: 'integration', + schema: sdk.z.object({ + appName: sdk.z.string().optional().title('Slack App Name').describe('The display name of the created Slack app'), + appConfigurationToken: sdk.z + .string() + .secret() + .title('Slack Configuration Token') + .describe('The Slack app configuration token used to create the app'), + appConfigurationRefreshToken: sdk.z + .string() + .secret() + .optional() + .title('Slack App Configuration Refresh Token') + .describe('Generated from api.slack.com/apps'), + appId: sdk.z.string().optional().title('Slack App ID').describe('The ID of the created Slack app'), + clientId: sdk.z.string().optional().title('Client ID').describe('OAuth Client ID from the manifest-created app'), + clientSecret: sdk.z + .string() + .secret() + .optional() + .title('Client Secret') + .describe('OAuth Client Secret from the manifest-created app'), + signingSecret: sdk.z + .string() + .secret() + .optional() + .title('Signing Secret') + .describe('Signing secret from the manifest-created app'), + authorizeUrl: sdk.z + .string() + .url() + .optional() + .title('Slack Authorize Url') + .describe('The default Slack app authorize url'), + }), + }, } as const satisfies sdk.IntegrationDefinitionProps['states'] diff --git a/integrations/slack/hub.md b/integrations/slack/hub.md index 61d0177d6ab..47186ce3bad 100644 --- a/integrations/slack/hub.md +++ b/integrations/slack/hub.md @@ -69,6 +69,29 @@ This is the simplest way to set up the integration. To set up the Slack integrat When using this configuration mode, a Botpress-managed Slack application will be used to connect to your workspace. The application will have the necessary permissions to send and receive messages, access channels, and perform other actions on your behalf. If you require more granular control over the permissions or prefer to use your own Slack application, you can opt for the manual configuration mode instead. +### App Manifest configuration (automatic setup) + +This configuration mode automatically creates a dedicated Slack app for your bot using the Slack App Manifest API. Unlike the default OAuth method which uses a shared Botpress-managed Slack app, this gives you your own Slack app with full control, without the manual setup steps of the manual configuration mode. + +#### Prerequisites + +- A Slack workspace where you have admin permissions. + +#### Steps + +1. Navigate to [api.slack.com/apps](https://api.slack.com/apps) and log in. +2. Scroll to the bottom of the page and find the **"Your App Configuration Tokens"** section. +3. Click **"Generate Token"** next to the workspace you want to install the bot in. Copy the generated token. Note that configuration tokens expire after 12 hours. +4. In Botpress, add the Slack integration to your bot. +5. Select the **"App Manifest (Automatic Setup)"** configuration mode. +6. Save the configuration. This will trigger the setup wizard. +7. In the wizard, click **Continue**, then paste your Configuration Token and submit. +8. The wizard will automatically create a Slack app with all required scopes, event subscriptions, and interactivity pre-configured. +9. You will be redirected to Slack to authorize the app. Click **"Allow"** to install it in your workspace. +10. Once complete, you will see a success page. The integration is now configured and ready to use. + +The created Slack app is fully yours and visible at [api.slack.com/apps](https://api.slack.com/apps). + ### Manual configuration with a bot token If you prefer to manually configure the integration, you can provide a bot token to connect your custom Slack application to Botpress. To set up the Slack integration manually, follow these steps: diff --git a/integrations/slack/integration.definition.ts b/integrations/slack/integration.definition.ts index 3807a21ace0..bbe05ae92a9 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: '4.0.1', + version: '4.0.2', icon: 'icon.svg', readme: 'hub.md', configuration, diff --git a/integrations/slack/manifestHandler.vrl b/integrations/slack/manifestHandler.vrl new file mode 100644 index 00000000000..23372049f7a --- /dev/null +++ b/integrations/slack/manifestHandler.vrl @@ -0,0 +1,4 @@ +webhookId = to_string!(.webhookId) +webhookUrl = to_string!(.webhookUrl) + +"{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}" diff --git a/integrations/slack/package.json b/integrations/slack/package.json index 8cea38673d1..5e201cac706 100644 --- a/integrations/slack/package.json +++ b/integrations/slack/package.json @@ -16,8 +16,8 @@ "@botpress/sdk": "workspace:*", "@botpress/sdk-addons": "workspace:*", "@bpinternal/slackdown": "^0.1.0", - "@slack/types": "^2.13.1", - "@slack/web-api": "^6.8.0", + "@slack/types": "^2.20.0", + "@slack/web-api": "^6.13.0", "axios": "^1.3.4", "fuse.js": "^6.6.2" }, diff --git a/integrations/slack/src/oauth-wizard/index.ts b/integrations/slack/src/oauth-wizard/index.ts new file mode 100644 index 00000000000..95a2a8bd3ac --- /dev/null +++ b/integrations/slack/src/oauth-wizard/index.ts @@ -0,0 +1,24 @@ +import { generateRedirection } from '@botpress/common/src/html-dialogs' +import { isOAuthWizardUrl, getInterstitialUrl } from '@botpress/common/src/oauth-wizard' +import * as wizard from './wizard' +import * as bp from '.botpress' + +export const oauthWizardHandler: bp.IntegrationProps['handler'] = async (props) => { + const { req, logger } = props + + if (!isOAuthWizardUrl(req.path)) { + return { + status: 404, + body: 'Invalid OAuth wizard endpoint', + } + } + + try { + return await wizard.handler(props) + } catch (thrown: unknown) { + const error = thrown instanceof Error ? thrown : Error(String(thrown)) + const errorMessage = 'OAuth wizard error: ' + error.message + logger.forBot().error(errorMessage) + return generateRedirection(getInterstitialUrl(false, errorMessage)) + } +} diff --git a/integrations/slack/src/oauth-wizard/wizard.ts b/integrations/slack/src/oauth-wizard/wizard.ts new file mode 100644 index 00000000000..8d5b21ad17c --- /dev/null +++ b/integrations/slack/src/oauth-wizard/wizard.ts @@ -0,0 +1,165 @@ +import * as oauthWizard from '@botpress/common/src/oauth-wizard' +import { RuntimeError, Response } from '@botpress/sdk' +import { + SlackManifestClient, + buildSlackAppManifest, + patchAppManifestConfigurationState, + getAppManifestConfigurationState, +} from '../slack-api/slack-manifest-client' +import { handleOAuthCallback } from '../webhook-events/handlers/oauth-callback' +import * as bp from '.botpress' + +type WizardHandler = oauthWizard.WizardStepHandler + +export const handler = async (props: bp.HandlerProps): Promise => { + const wizard = new oauthWizard.OAuthWizardBuilder(props) + .addStep({ id: 'start', handler: _startHandler }) + .addStep({ id: 'get-config-token', handler: _getConfigTokenHandler }) + .addStep({ id: 'save-config-token', handler: _saveConfigTokenHandler }) + .addStep({ id: 'get-config-refresh-token', handler: _getConfigRefreshTokenHandler }) + .addStep({ id: 'save-config-refresh-token', handler: _saveConfigRefreshTokenHandler }) + .addStep({ id: 'get-app-name', handler: _getAppNameHandler }) + .addStep({ id: 'save-app-name', handler: _saveAppNameHandler }) + .addStep({ id: 'create-app', handler: _createAppHandler }) + .addStep({ id: 'oauth-callback', handler: _oauthCallbackHandler }) + .addStep({ id: 'end', handler: _endHandler }) + .build() + + return await wizard.handleRequest() +} + +const _startHandler: WizardHandler = async ({ client, ctx, responses }) => { + const { appConfigurationToken, appConfigurationRefreshToken, appName, appId, clientId, clientSecret, authorizeUrl } = + await getAppManifestConfigurationState(client, ctx) + + if (appConfigurationToken && appConfigurationRefreshToken && !appName) { + return responses.redirectToStep('get-app-name') + } + + if (appId && clientId && clientSecret && authorizeUrl) { + return responses.redirectToExternalUrl(authorizeUrl) + } + + return responses.displayButtons({ + pageTitle: 'Slack App Setup', + htmlOrMarkdownPageContents: + 'This wizard will create a dedicated Slack app for your bot using the Slack App Manifest API.

' + + 'A Slack app will be automatically created and configured with all the required permissions and settings.', + buttons: [ + { action: 'navigate', label: 'Continue', navigateToStep: 'get-config-token', buttonType: 'primary' }, + { action: 'close', label: 'Cancel', buttonType: 'secondary' }, + ], + }) +} + +const _getConfigTokenHandler: WizardHandler = async ({ client, ctx, responses }) => { + const state = await getAppManifestConfigurationState(client, ctx) + if (state.appConfigurationToken && state.appConfigurationRefreshToken) { + return responses.redirectToStep('get-app-name') + } + + return responses.displayInput({ + pageTitle: 'Slack App Configuration Token', + htmlOrMarkdownPageContents: + 'Enter your Slack App Configuration Token.
You can generate one at api.slack.com/apps.', + input: { label: 'App Configuration Token', type: 'text' }, + nextStepId: 'save-config-token', + }) +} + +const _saveConfigTokenHandler: WizardHandler = async ({ client, ctx, responses, inputValue }) => { + if (!inputValue?.trim()) { + return responses.redirectToStep('get-config-token') + } + await patchAppManifestConfigurationState(client, ctx, { appConfigurationToken: inputValue.trim() }) + return responses.redirectToStep('get-config-refresh-token') +} + +const _getConfigRefreshTokenHandler: WizardHandler = (props) => { + return props.responses.displayInput({ + pageTitle: 'Slack App Configuration Refresh Token', + htmlOrMarkdownPageContents: + 'Enter your Slack App Configuration Refresh Token.
You can generate one at api.slack.com/apps.', + input: { label: 'App Configuration Refresh Token', type: 'text' }, + nextStepId: 'save-config-refresh-token', + }) +} + +const _saveConfigRefreshTokenHandler: WizardHandler = async ({ client, ctx, responses, inputValue }) => { + if (!inputValue?.trim()) { + return responses.redirectToStep('get-config-refresh-token') + } + await patchAppManifestConfigurationState(client, ctx, { appConfigurationRefreshToken: inputValue.trim() }) + return responses.redirectToStep('get-app-name') +} + +const _getAppNameHandler: WizardHandler = (props) => { + return props.responses.displayInput({ + pageTitle: 'Name Your Slack App', + htmlOrMarkdownPageContents: 'Choose a name for the Slack app that will be created (max 35 characters).', + input: { label: 'e.g. My Botpress Bot', type: 'text' }, + nextStepId: 'save-app-name', + }) +} + +const _saveAppNameHandler: WizardHandler = async ({ client, ctx, responses, inputValue }) => { + if (!inputValue || inputValue?.trim().length > 35) { + return responses.redirectToStep('get-app-name') + } + await patchAppManifestConfigurationState(client, ctx, { appName: inputValue.trim() }) + + return responses.redirectToStep('create-app') +} + +const _createAppHandler: WizardHandler = async (props) => { + const { client, ctx, responses, logger } = props + + if (ctx.configurationType !== 'manifestAppCredentials') { + throw new RuntimeError('This wizard is only available for the App Manifest configuration type') + } + + const manifestState = await getAppManifestConfigurationState(client, ctx) + + if (!manifestState.appConfigurationToken || !manifestState.appConfigurationRefreshToken) { + throw new RuntimeError('Slack App Configuration Token and Refresh Token are required. Please restart the wizard.') + } + const appName = manifestState.appName || 'Botpress Bot' + + const webhookUrl = process.env.BP_WEBHOOK_URL! + const redirectUri = `${webhookUrl}/oauth` + const manifest = buildSlackAppManifest(webhookUrl, redirectUri, appName) + const manifestClient = await SlackManifestClient.create({ + client, + ctx, + logger, + }) + + logger.forBot().debug('Validating Slack app manifest...') + await manifestClient.validateManifest(manifest) + + logger.forBot().debug('Creating Slack app from manifest...') + const { app_id, credentials, oauth_authorize_url } = await manifestClient.createApp(manifest) + const authorizeUrl = new URL(oauth_authorize_url) + const oauthCallbackUrl = oauthWizard.getWizardStepUrl('oauth-callback').toString() + authorizeUrl.searchParams.set('redirect_uri', oauthCallbackUrl) + authorizeUrl.searchParams.set('state', ctx.webhookId) + + await patchAppManifestConfigurationState(client, ctx, { + appId: app_id, + clientId: credentials.client_id, + clientSecret: credentials.client_secret, + signingSecret: credentials.signing_secret, + authorizeUrl: authorizeUrl.toString(), + }) + return responses.redirectToExternalUrl(authorizeUrl.toString()) +} + +const _oauthCallbackHandler: WizardHandler = async ({ req, client, ctx, logger, responses }) => { + await handleOAuthCallback({ req, client, ctx, logger }) + + return responses.redirectToStep('end') +} + +const _endHandler: WizardHandler = ({ responses }) => { + return responses.endWizard({ success: true }) +} diff --git a/integrations/slack/src/setup.ts b/integrations/slack/src/setup.ts index 42f16c1ccdd..ad0dde45524 100644 --- a/integrations/slack/src/setup.ts +++ b/integrations/slack/src/setup.ts @@ -1,9 +1,9 @@ -import * as sdk from '@botpress/client' +import { RuntimeError } from '@botpress/sdk' import { isValidUrl } from './misc/utils' import { SlackClient } from './slack-api' import type * as bp from '.botpress' -const REQUIRED_SLACK_SCOPES = [ +export const REQUIRED_SLACK_SCOPES = [ 'channels:history', 'channels:manage', 'channels:read', @@ -39,7 +39,7 @@ export const register: bp.IntegrationProps['register'] = async ({ client, ctx, l !ctx.configuration.clientId || !ctx.configuration.clientSecret ) { - throw new sdk.RuntimeError( + throw new RuntimeError( 'Missing configuration: Refresh Token, Signing Secret, Client ID, and Client Secret are all required when using manual configuration' ) } @@ -82,7 +82,7 @@ export const register: bp.IntegrationProps['register'] = async ({ client, ctx, l const grantedScopes = slackClient.getGrantedScopes() const missingScopes = REQUIRED_SLACK_SCOPES.filter((scope) => !grantedScopes.includes(scope)) - throw new sdk.RuntimeError( + throw new RuntimeError( 'The Slack access token is missing required scopes. Please re-authorize the app.\n\n' + `Missing scopes: ${missingScopes.join(', ')}.\n` + `Granted scopes: ${grantedScopes.join(', ')}.` @@ -122,24 +122,22 @@ const _validateTokenType = (ctx: bp.Context) => { if (ctx.configurationType !== 'refreshToken') return if (ctx.configuration.refreshToken.startsWith('xapp-')) { - throw new sdk.RuntimeError( + throw new RuntimeError( 'App-level tokens (tokens beginning with xapp) are not supported. Please provide either a bot refresh token or a bot access token.' ) } else if (ctx.configuration.refreshToken.startsWith('xoxp-')) { - throw new sdk.RuntimeError( + throw new RuntimeError( 'User tokens (tokens beginning with xoxp) are not supported. Please provide either a bot refresh token or a bot access token.' ) } else if (ctx.configuration.refreshToken.startsWith('xoxe.xoxb-1-')) { - throw new sdk.RuntimeError( + throw new RuntimeError( 'Rotating bot tokens (tokens beginning with xoxe.xoxb) are not supported. Please provide either a bot refresh token or a bot access token.' ) } else if ( !ctx.configuration.refreshToken.startsWith('xoxe-1-') && !ctx.configuration.refreshToken.startsWith('xoxb-') ) { - throw new sdk.RuntimeError( - 'Unknown Slack token type. Please provide either a bot refresh token or a bot access token.' - ) + throw new RuntimeError('Unknown Slack token type. Please provide either a bot refresh token or a bot access token.') } } diff --git a/integrations/slack/src/slack-api/card-renderer.ts b/integrations/slack/src/slack-api/card-renderer.ts index 45957762b87..6c40d838d05 100644 --- a/integrations/slack/src/slack-api/card-renderer.ts +++ b/integrations/slack/src/slack-api/card-renderer.ts @@ -38,29 +38,29 @@ export const renderCard = (payload: Card): ChatPostMessageArguments['blocks'] => ] const _renderButtonUrl = (action: CardAction) => ({ - type: 'button', + type: 'button' as const, text: { - type: 'plain_text', + type: 'plain_text' as const, text: action.label, }, url: action.value, }) const _renderButtonPostback = (action: CardAction) => ({ - type: 'button', + type: 'button' as const, action_id: 'postback', text: { - type: 'plain_text', + type: 'plain_text' as const, text: action.label, }, value: action.value, }) const _renderButtonSay = (action: CardAction) => ({ - type: 'button', + type: 'button' as const, action_id: 'say', text: { - type: 'plain_text', + type: 'plain_text' as const, text: action.label, }, value: action.value, diff --git a/integrations/slack/src/slack-api/error-handling.ts b/integrations/slack/src/slack-api/error-handling.ts index cc1c8f24598..08c7d92c29d 100644 --- a/integrations/slack/src/slack-api/error-handling.ts +++ b/integrations/slack/src/slack-api/error-handling.ts @@ -1,6 +1,7 @@ import { createAsyncFnWrapperWithErrorRedaction, createErrorHandlingDecorator } from '@botpress/common' import * as sdk from '@botpress/sdk' import * as slackWebApi from '@slack/web-api' +import { Logger as BPLogger } from '.botpress' export const redactSlackError = (thrown: unknown, genericErrorMessage: string): sdk.RuntimeError => { const error = thrown instanceof Error ? thrown : new Error(String(thrown)) @@ -46,3 +47,25 @@ export const redactSlackError = (thrown: unknown, genericErrorMessage: string): export const wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction(redactSlackError) export const handleErrorsDecorator = createErrorHandlingDecorator(wrapAsyncFnWithTryCatch) + +export const surfaceSlackErrors = ({ + logger, + response, +}: { + logger: BPLogger + response: TResponse +}): TResponse => { + if (response.response_metadata?.warnings?.length) { + logger.forBot().warn('Slack API emitted warnings', response.response_metadata.warnings) + } + + if (response.error) { + throw new sdk.RuntimeError(`Slack API error: ${response.error}`) + } + + if (!response.ok) { + throw new sdk.RuntimeError('Slack API returned an unspecified error') + } + + return response +} diff --git a/integrations/slack/src/slack-api/index.ts b/integrations/slack/src/slack-api/index.ts index ef44e704bf5..5aa60f6a3e2 100644 --- a/integrations/slack/src/slack-api/index.ts +++ b/integrations/slack/src/slack-api/index.ts @@ -1,2 +1,3 @@ export { SlackClient } from './slack-client' +export { SlackManifestClient, buildSlackAppManifest } from './slack-manifest-client' export { wrapAsyncFnWithTryCatch } from './error-handling' diff --git a/integrations/slack/src/slack-api/slack-client.ts b/integrations/slack/src/slack-api/slack-client.ts index 5f9d696f3bb..34c1f03eaed 100644 --- a/integrations/slack/src/slack-api/slack-client.ts +++ b/integrations/slack/src/slack-api/slack-client.ts @@ -1,7 +1,7 @@ import { collectableGenerator } from '@botpress/common' import * as sdk from '@botpress/sdk' import * as SlackWebApi from '@slack/web-api' -import { handleErrorsDecorator as handleErrors } from './error-handling' +import { handleErrorsDecorator as handleErrors, surfaceSlackErrors } from './error-handling' import { SlackOAuthClient } from './slack-oauth-client' import { requiresAllScopesDecorator as requireAllScopes } from './slack-scopes' import * as bp from '.botpress' @@ -36,8 +36,29 @@ export class SlackClient { ctx: bp.Context logger: bp.Logger }) { + if (ctx.configurationType === 'manifestAppCredentials') { + const { + state: { + payload: { clientId, clientSecret }, + }, + } = await client.getState({ + type: 'integration', + name: 'manifestAppCredentials', + id: ctx.integrationId, + }) + if (!clientId || !clientSecret) { + throw new sdk.RuntimeError('Client ID or Client Secret not found, please re-run the authorization wizard') + } + const oAuthClient = new SlackOAuthClient({ + ctx, + client, + logger, + clientIdOverride: clientId, + clientSecretOverride: clientSecret, + }) + return await SlackClient._createNewInstance({ logger, oAuthClient }) + } const oAuthClient = new SlackOAuthClient({ ctx, client, logger }) - return await SlackClient._createNewInstance({ logger, oAuthClient }) } @@ -52,6 +73,35 @@ export class SlackClient { logger: bp.Logger authorizationCode: string }) { + if (ctx.configurationType === 'manifestAppCredentials') { + const { + state: { + payload: { clientId, clientSecret, authorizeUrl }, + }, + } = await client.getState({ + type: 'integration', + name: 'manifestAppCredentials', + id: ctx.integrationId, + }) + if (!clientId || !clientSecret || !authorizeUrl) { + throw new sdk.RuntimeError('Client ID or Client Secret not found, please re-run the authorization wizard') + } + const oAuthClient = new SlackOAuthClient({ + ctx, + client, + logger, + clientIdOverride: clientId, + clientSecretOverride: clientSecret, + }) + const redirectUri = new URL(authorizeUrl).searchParams.get('redirect_uri') + if (!redirectUri) { + throw new sdk.RuntimeError('Could not retreive redirect uri, please re-run the authorization wizard') + } + await oAuthClient.requestShortLivedCredentials.fromAuthorizationCode(authorizationCode, redirectUri!) + + return await SlackClient._createNewInstance({ logger, oAuthClient }) + } + const oAuthClient = new SlackOAuthClient({ ctx, client, logger }) await oAuthClient.requestShortLivedCredentials.fromAuthorizationCode(authorizationCode) @@ -108,7 +158,10 @@ export class SlackClient { @handleErrors('Failed to validate Slack authentication') public async testAuthentication() { - this._surfaceSlackErrors(await this._slackWebClient.auth.test()) + surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.auth.test(), + }) } public getBotUserId(): string { @@ -172,12 +225,13 @@ export class SlackClient { let resultCount = 0 do { - const { members, response_metadata } = this._surfaceSlackErrors( - await this._slackWebClient.users.list({ + const { members, response_metadata } = surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.users.list({ cursor, limit: 200, - }) - ) + }), + }) for (const member of members ?? []) { if (limit && resultCount++ > limit) { @@ -201,14 +255,15 @@ export class SlackClient { let resultCount = 0 do { - const { channels, response_metadata } = this._surfaceSlackErrors( - await this._slackWebClient.conversations.list({ + const { channels, response_metadata } = surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.conversations.list({ exclude_archived: true, cursor, limit: 200, types: 'public_channel', - }) - ) + }), + }) for (const channel of channels ?? []) { if (limit && resultCount++ > limit) { @@ -224,14 +279,15 @@ export class SlackClient { @requireAllScopes(['channels:history', 'groups:history', 'im:history', 'mpim:history']) @handleErrors('Failed to retrieve message') public async retrieveMessage({ channel, messageTs }: { channel: string; messageTs: string }) { - const { messages } = this._surfaceSlackErrors( - await this._slackWebClient.conversations.history({ + const { messages } = surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.conversations.history({ channel, limit: 1, inclusive: true, latest: messageTs, - }) - ) + }), + }) const message = messages?.[0] @@ -245,11 +301,12 @@ export class SlackClient { @requireAllScopes(['im:write']) @handleErrors('Failed to start DM with user') public async startDmWithUser(userId: string) { - const { channel } = this._surfaceSlackErrors( - await this._slackWebClient.conversations.open({ + const { channel } = surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.conversations.open({ users: userId, - }) - ) + }), + }) const dmChannelId = channel?.id @@ -295,8 +352,9 @@ export class SlackClient { username?: string iconUrl?: string }) { - const response = this._surfaceSlackErrors( - await this._slackWebClient.chat.postMessage({ + const response = surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.chat.postMessage({ channel: channelId, text, thread_ts: threadTs, @@ -304,8 +362,8 @@ export class SlackClient { as_user: true, username, icon_url: iconUrl, - }) - ) + }), + }) return response.message } @@ -313,11 +371,12 @@ export class SlackClient { @requireAllScopes(['users:read']) @handleErrors('Failed to retrieve user profile') public async getUserProfile({ userId }: { userId: string }) { - const response = this._surfaceSlackErrors( - await this._slackWebClient.users.profile.get({ + const response = surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.users.profile.get({ user: userId, - }) - ) + }), + }) return response.profile } @@ -343,20 +402,4 @@ export class SlackClient { return undefined } - - private _surfaceSlackErrors(response: TResponse): TResponse { - if (response.response_metadata?.warnings?.length) { - this._logger.forBot().warn('Slack API emitted warnings', response.response_metadata.warnings) - } - - if (response.error) { - throw new sdk.RuntimeError(`Slack API error: ${response.error}`) - } - - if (!response.ok) { - throw new sdk.RuntimeError('Slack API returned an unspecified error') - } - - return response - } } diff --git a/integrations/slack/src/slack-api/slack-manifest-client.ts b/integrations/slack/src/slack-api/slack-manifest-client.ts new file mode 100644 index 00000000000..72fd883af55 --- /dev/null +++ b/integrations/slack/src/slack-api/slack-manifest-client.ts @@ -0,0 +1,275 @@ +import { z, RuntimeError } from '@botpress/sdk' +import * as SlackWebClient from '@slack/web-api' +import { states } from 'definitions' +import { REQUIRED_SLACK_SCOPES } from '../setup' +import { surfaceSlackErrors } from './error-handling' +import * as bp from '.botpress' + +const BOT_EVENTS = [ + 'message.im', + 'message.groups', + 'message.mpim', + 'message.channels', + 'reaction_added', + 'reaction_removed', + 'member_joined_channel', + 'member_left_channel', + 'team_join', +] + +const manifestSchema = z.object({ + display_information: z.object({ + name: z.string(), + }), + features: z.object({ + bot_user: z.object({ + display_name: z.string(), + always_online: z.boolean(), + }), + }), + oauth_config: z.object({ + scopes: z.object({ + bot: z.array(z.string()), + }), + redirect_urls: z.array(z.string()), + token_management_enabled: z.boolean(), + }), + settings: z.object({ + event_subscriptions: z.object({ + request_url: z.string(), + bot_events: z.array(z.string()), + }), + interactivity: z.object({ + is_enabled: z.boolean(), + request_url: z.string(), + }), + org_deploy_enabled: z.boolean(), + socket_mode_enabled: z.boolean(), + token_rotation_enabled: z.boolean(), + }), +}) + +const manifestCreateResponseSchema = z.object({ + ok: z.literal(true), + app_id: z.string(), + credentials: z.object({ + client_id: z.string(), + client_secret: z.string(), + signing_secret: z.string(), + }), + oauth_authorize_url: z.string(), +}) + +export type ManifestCreateResponse = z.infer +export type SlackAppManifest = z.infer + +type ManifestAppCredentialsState = bp.states.manifestAppCredentials.ManifestAppCredentials['payload'] +type AppManifestConfigurationCredentials = Pick< + ManifestAppCredentialsState, + 'appConfigurationToken' | 'appConfigurationRefreshToken' +> + +export const patchAppManifestConfigurationState = async ( + client: bp.Client, + ctx: bp.Context, + newState: Partial +) => { + const state = await getAppManifestConfigurationState(client, ctx) + const { data, success } = states.manifestAppCredentials.schema.safeParse({ + ...state, + ...newState, + }) + if (!success) { + throw new RuntimeError(`Failed to parse manifest app credentials state: ${JSON.stringify(data)}`) + } + await client.setState({ + type: 'integration', + id: ctx.integrationId, + name: 'manifestAppCredentials', + payload: data, + }) +} + +export const getAppManifestConfigurationState = async ( + client: bp.Client, + ctx: bp.Context +): Promise> => { + try { + const { state } = await client.getState({ + type: 'integration', + name: 'manifestAppCredentials', + id: ctx.integrationId, + }) + return state.payload + } catch { + return {} + } +} + +export class SlackManifestClient { + private readonly _logger: bp.Logger + private readonly _appConfigurationToken: string + private readonly _slackWebClient: SlackWebClient.WebClient + + private constructor(appConfigurationToken: string, logger: bp.Logger) { + this._logger = logger + this._appConfigurationToken = appConfigurationToken + this._slackWebClient = new SlackWebClient.WebClient(this._appConfigurationToken) + } + + public static async create(props: bp.CommonHandlerProps) { + const validToken = await SlackManifestClient._resolveAndValidateToken(props) + return new SlackManifestClient(validToken, props.logger) + } + + private static async _resolveTokens({ + client, + ctx, + }: bp.CommonHandlerProps): Promise { + if (ctx.configurationType !== 'manifestAppCredentials') { + throw new RuntimeError('Slack manifest app credentials are not properly configured') + } + + const state = await getAppManifestConfigurationState(client, ctx) + + const appConfigurationToken = state.appConfigurationToken + const appConfigurationRefreshToken = state.appConfigurationRefreshToken + + if (!appConfigurationToken || !appConfigurationRefreshToken) { + throw new RuntimeError('Slack manifest app credentials are not properly configured') + } + + return { appConfigurationToken, appConfigurationRefreshToken } + } + + private static async _resolveAndValidateToken(props: bp.CommonHandlerProps): Promise { + const { client, ctx, logger } = props + const { appConfigurationToken, appConfigurationRefreshToken } = await SlackManifestClient._resolveTokens(props) + + const slackWebClient = new SlackWebClient.WebClient() + logger.forBot().debug('Validating Slack app configuration token and refresh token...') + const { ok, error } = await slackWebClient.auth + .test({ + token: appConfigurationToken, + }) + .catch((e) => e.data as SlackWebClient.AuthTestResponse) + + if (!ok && error === 'token_expired') { + logger + .forBot() + .debug('Slack app configuration token has expired, attempting to rotate the token using the refresh token...') + const { token: rotatedToken, refresh_token } = surfaceSlackErrors({ + logger, + response: await slackWebClient.tooling.tokens.rotate({ + refresh_token: appConfigurationRefreshToken!, + }), + }) + logger.forBot().debug('Rotating Slack app configuration token...') + await patchAppManifestConfigurationState(client, ctx, { + appConfigurationToken: rotatedToken, + appConfigurationRefreshToken: refresh_token, + }) + logger.forBot().debug('Rotated expired Slack app configuration token successfully') + return rotatedToken! + } else if (!ok) { + throw new RuntimeError(`Failed to validate Slack app configuration token: ${error}`) + } + + return appConfigurationToken + } + + public async validateManifest(manifest: SlackAppManifest) { + surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.apps.manifest.validate({ + token: this._appConfigurationToken, + manifest: JSON.stringify(manifest), + }), + }) + } + + public async createApp(manifest: SlackAppManifest): Promise { + const response = surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.apps.manifest.create({ + token: this._appConfigurationToken, + manifest: JSON.stringify(manifest), + }), + }) + + const { data, success } = manifestCreateResponseSchema.safeParse(response) + if (!success) { + throw new RuntimeError(`Unexpected response from Slack manifest create API: ${JSON.stringify(response)}`) + } + return data + } + + public async exportApp(appId: string): Promise { + const response = surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.apps.manifest.export({ + token: this._appConfigurationToken, + app_id: appId, + }), + }) + const { data, success } = manifestSchema.safeParse(response.manifest) + if (!success) { + throw new RuntimeError(`Unexpected response from Slack manifest export API: ${JSON.stringify(response.manifest)}`) + } + return data + } + + public async updateApp(appId: string, manifest: SlackAppManifest): Promise { + surfaceSlackErrors({ + logger: this._logger, + response: await this._slackWebClient.apps.manifest.update({ + token: this._appConfigurationToken, + app_id: appId, + manifest: JSON.stringify(manifest), + }), + }) + } + + public async updateAppIfNeeded(appId: string, desiredManifest: SlackAppManifest): Promise { + const currentManifest = await this.exportApp(appId) + if (JSON.stringify(currentManifest) === JSON.stringify(desiredManifest)) { + this._logger.forBot().debug('Slack app manifest is up to date, skipping update') + return false + } + this._logger.forBot().debug('Slack app manifest has changed, updating...') + await this.updateApp(appId, desiredManifest) + return true + } +} + +export const buildSlackAppManifest = (webhookUrl: string, redirectUri: string, appName: string): SlackAppManifest => ({ + display_information: { + name: appName, + }, + features: { + bot_user: { + display_name: appName, + always_online: true, + }, + }, + oauth_config: { + scopes: { + bot: REQUIRED_SLACK_SCOPES, + }, + redirect_urls: [redirectUri], + token_management_enabled: true, + }, + settings: { + event_subscriptions: { + request_url: webhookUrl, + bot_events: BOT_EVENTS, + }, + interactivity: { + is_enabled: true, + request_url: webhookUrl, + }, + org_deploy_enabled: false, + socket_mode_enabled: false, + token_rotation_enabled: true, + }, +}) diff --git a/integrations/slack/src/slack-api/slack-oauth-client.ts b/integrations/slack/src/slack-api/slack-oauth-client.ts index 06e1b5a4a43..ae357645ce9 100644 --- a/integrations/slack/src/slack-api/slack-oauth-client.ts +++ b/integrations/slack/src/slack-api/slack-oauth-client.ts @@ -35,10 +35,24 @@ export class SlackOAuthClient { private readonly _slackClient: SlackWebClient private _currentAuthState: PrivateAuthState | undefined = undefined - public constructor({ ctx, client, logger }: { client: bp.Client; ctx: bp.Context; logger: bp.Logger }) { - this._clientId = ctx.configurationType === 'refreshToken' ? ctx.configuration.clientId : bp.secrets.CLIENT_ID + public constructor({ + ctx, + client, + logger, + clientIdOverride, + clientSecretOverride, + }: { + client: bp.Client + ctx: bp.Context + logger: bp.Logger + clientIdOverride?: string + clientSecretOverride?: string + }) { + this._clientId = + clientIdOverride ?? (ctx.configurationType === 'refreshToken' ? ctx.configuration.clientId : bp.secrets.CLIENT_ID) this._clientSecret = - ctx.configurationType === 'refreshToken' ? ctx.configuration.clientSecret : bp.secrets.CLIENT_SECRET + clientSecretOverride ?? + (ctx.configurationType === 'refreshToken' ? ctx.configuration.clientSecret : bp.secrets.CLIENT_SECRET) this._slackClient = new SlackWebClient() this._client = client this._ctx = ctx @@ -66,15 +80,18 @@ export class SlackOAuthClient { * Exchanges an OAuth callback authorization code for short-lived rotating * credentials. This code path is used once when the user selects automatic * configuration using our Botpress Slack app. + * + * @param authorizationCode - The authorization code from Slack's OAuth callback + * @param redirectUri - Optional custom redirect URI. Defaults to `${process.env.BP_WEBHOOK_URL}/oauth` */ - fromAuthorizationCode: async (authorizationCode: string) => { + fromAuthorizationCode: async (authorizationCode: string, redirectUri?: string) => { this._logger.forBot().debug('Exchanging authorization code for short-lived credentials...') const response = await this._slackClient.oauth.v2 .access({ client_id: this._clientId, client_secret: this._clientSecret, - redirect_uri: `${process.env.BP_WEBHOOK_URL}/oauth`, + redirect_uri: redirectUri ?? `${process.env.BP_WEBHOOK_URL}/oauth`, grant_type: 'authorization_code', code: authorizationCode, }) diff --git a/integrations/slack/src/webhook-events/handler-dispatcher.ts b/integrations/slack/src/webhook-events/handler-dispatcher.ts index 45250d75fd1..79cf5254a4d 100644 --- a/integrations/slack/src/webhook-events/handler-dispatcher.ts +++ b/integrations/slack/src/webhook-events/handler-dispatcher.ts @@ -1,6 +1,9 @@ +import { isOAuthWizardUrl } from '@botpress/common/src/oauth-wizard' import * as sdk from '@botpress/sdk' import type { SlackEvent } from '@slack/types' import { safeParseBody } from 'src/misc/utils' +import { oauthWizardHandler } from '../oauth-wizard' +import { getAppManifestConfigurationState } from '../slack-api/slack-manifest-client' import * as handlers from './handlers' import { handleInteractiveRequest, isInteractiveRequest } from './handlers/interactive-request' import { isOAuthCallback, handleOAuthCallback } from './handlers/oauth-callback' @@ -11,6 +14,10 @@ import * as bp from '.botpress' export const handler: bp.IntegrationProps['handler'] = async ({ req, ctx, client, logger }) => { logger.forBot().debug('Handler received request from Slack with payload:', req.body) + if (isOAuthWizardUrl(req.path)) { + return await oauthWizardHandler({ req, client, logger, ctx }) + } + if (isOAuthCallback(req)) { return await handleOAuthCallback({ req, client, logger, ctx }) } @@ -53,8 +60,8 @@ function _verifyBodyIsPresent(req: sdk.Request): asserts req is sdk.Request & { } } -const _verifyMessageIsProperlyAuthenticated = async ({ req, logger, ctx }: bp.HandlerProps) => { - const signingSecret = _getSigningSecret(ctx) +const _verifyMessageIsProperlyAuthenticated = async ({ req, logger, ...props }: bp.HandlerProps) => { + const signingSecret = await _getSigningSecret({ logger, ...props }) const isSignatureValid = new SlackEventSignatureValidator(signingSecret, req, logger).isEventProperlyAuthenticated() if (!isSignatureValid) { @@ -107,5 +114,13 @@ const _dispatchEvent = async ({ client, ctx, logger }: bp.HandlerProps, slackEve } } -const _getSigningSecret = (ctx: bp.Context) => - ctx.configurationType === 'refreshToken' ? ctx.configuration.signingSecret : bp.secrets.SIGNING_SECRET +const _getSigningSecret = async ({ client, ctx }: bp.CommonHandlerProps): Promise => { + if (ctx.configurationType === 'refreshToken') { + return ctx.configuration.signingSecret + } else if (ctx.configurationType === 'manifestAppCredentials') { + const { signingSecret } = await getAppManifestConfigurationState(client, ctx) + if (!signingSecret) throw new sdk.RuntimeError('Signing secret not found, please re-run setup wizard') + return signingSecret + } + return bp.secrets.SIGNING_SECRET +} diff --git a/integrations/slack/src/webhook-events/handlers/message-received.ts b/integrations/slack/src/webhook-events/handlers/message-received.ts index bb2c3f8fdb2..53ce4b3451e 100644 --- a/integrations/slack/src/webhook-events/handlers/message-received.ts +++ b/integrations/slack/src/webhook-events/handlers/message-received.ts @@ -356,7 +356,7 @@ const _getTextPayloadFromSlackEvent = async ( type BlockElement = ContextBlockElement | ActionsBlockElement | RichTextBlockElement type BlockSubElement = RichTextSection | RichTextElement const userElements = blocks - .flatMap((block): BlockElement[] => ('elements' in block ? block.elements : [])) + .flatMap((block): BlockElement[] => ('elements' in block ? (block.elements as BlockElement[]) : [])) .flatMap((element): BlockSubElement[] => ('elements' in element ? element.elements : [])) .filter((subElement) => subElement.type === 'user') diff --git a/integrations/slack/src/webhook-events/handlers/oauth-callback.ts b/integrations/slack/src/webhook-events/handlers/oauth-callback.ts index 0f3deb38822..f5e6fabade7 100644 --- a/integrations/slack/src/webhook-events/handlers/oauth-callback.ts +++ b/integrations/slack/src/webhook-events/handlers/oauth-callback.ts @@ -2,8 +2,7 @@ import * as sdk from '@botpress/sdk' import { SlackClient } from 'src/slack-api' import * as bp from '.botpress' -export const isOAuthCallback = (req: sdk.Request): req is sdk.Request & { path: '/oauth' } => - req.path.startsWith('/oauth') +export const isOAuthCallback = (req: sdk.Request): req is sdk.Request & { path: '/oauth' } => req.path === '/oauth' export const handleOAuthCallback = async ({ req, client, ctx, logger }: bp.HandlerProps) => { const query = new URLSearchParams(req.query) diff --git a/integrations/slack/tsconfig.json b/integrations/slack/tsconfig.json index 758f7d7ec50..141f6a2a036 100644 --- a/integrations/slack/tsconfig.json +++ b/integrations/slack/tsconfig.json @@ -4,7 +4,9 @@ "baseUrl": ".", "outDir": "dist", "experimentalDecorators": true, - "emitDecoratorMetadata": true + "emitDecoratorMetadata": true, + "jsx": "react-jsx", + "jsxImportSource": "preact" }, "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 774b3c24ed6..ba5f64f7d14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1691,11 +1691,11 @@ importers: specifier: ^0.1.0 version: 0.1.0 '@slack/types': - specifier: ^2.13.1 - version: 2.13.1 + specifier: ^2.20.0 + version: 2.20.0 '@slack/web-api': - specifier: ^6.8.0 - version: 6.8.1 + specifier: ^6.13.0 + version: 6.13.0 axios: specifier: ^1.3.4 version: 1.4.0 @@ -3702,12 +3702,16 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.26.8': resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.0': - resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} '@babel/core@7.26.9': @@ -3726,8 +3730,8 @@ packages: resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.1': @@ -3738,8 +3742,8 @@ packages: resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} '@babel/helper-create-class-features-plugin@7.27.1': @@ -3764,6 +3768,10 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.26.0': resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} @@ -3776,8 +3784,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -3816,6 +3824,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.9': resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} @@ -3828,8 +3840,8 @@ packages: resolution: {integrity: sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.3': - resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} engines: {node: '>=6.9.0'} '@babel/parser@7.26.9': @@ -3842,8 +3854,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.3': - resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true @@ -3960,6 +3972,10 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.9': resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} engines: {node: '>=6.9.0'} @@ -3968,8 +3984,8 @@ packages: resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.3': - resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} '@babel/types@7.26.9': @@ -3980,8 +3996,8 @@ packages: resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.2': - resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} '@bcherny/json-schema-ref-parser@10.0.5-fork': @@ -4929,6 +4945,9 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -5625,16 +5644,12 @@ packages: resolution: {integrity: sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==} engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - '@slack/types@2.13.1': - resolution: {integrity: sha512-YVtJCVtDcjOPKsvOedIThb7YmKNCcSoZN0mUSQqD2fc2ZyI59gOLCF4rYGfw/0C0agzFxAmb7hV5tbMGrgK0Tg==} - engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - - '@slack/types@2.8.0': - resolution: {integrity: sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ==} + '@slack/types@2.20.0': + resolution: {integrity: sha512-PVF6P6nxzDMrzPC8fSCsnwaI+kF8YfEpxf3MqXmdyjyWTYsZQURpkK7WWUWvP5QpH55pB7zyYL9Qem/xSgc5VA==} engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - '@slack/web-api@6.8.1': - resolution: {integrity: sha512-eMPk2S99S613gcu7odSw/LV+Qxr8A+RXvBD0GYW510wJuTERiTjP5TgCsH8X09+lxSumbDE88wvWbuFuvGa74g==} + '@slack/web-api@6.13.0': + resolution: {integrity: sha512-dv65crIgdh9ZYHrevLU6XFHTQwTyDmNqEqzuIrV+Vqe/vgiG6w37oex5ePDU1RGm2IJ90H8iOvHFvzdEO/vB+g==} engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} '@smithy/abort-controller@2.2.0': @@ -6715,6 +6730,9 @@ packages: axios@1.13.1: resolution: {integrity: sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==} + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + axios@1.2.5: resolution: {integrity: sha512-9pU/8mmjSSOb4CXVsvGIevN+MlO/t9OWtKadTaLuN85Gge3HGorUckgp8A/2FH4V4hJ7JuQ3LIeI7KAV9ITZrQ==} @@ -6804,6 +6822,11 @@ packages: resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} engines: {node: '>=6.0.0'} + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} @@ -6890,8 +6913,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.25.4: - resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -7000,8 +7023,8 @@ packages: caniuse-lite@1.0.30001700: resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} - caniuse-lite@1.0.30001739: - resolution: {integrity: sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==} + caniuse-lite@1.0.30001774: + resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -7315,6 +7338,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@5.0.1: resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} engines: {node: '>=10'} @@ -7524,8 +7556,8 @@ packages: electron-to-chromium@1.5.104: resolution: {integrity: sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==} - electron-to-chromium@1.5.214: - resolution: {integrity: sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==} + electron-to-chromium@1.5.302: + resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -7987,6 +8019,15 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + follow-redirects@1.15.2: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -8052,6 +8093,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -8585,8 +8630,8 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true - is-electron@2.2.0: - resolution: {integrity: sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q==} + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -9730,6 +9775,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + nodemailer@6.9.3: resolution: {integrity: sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==} engines: {node: '>=6.0.0'} @@ -10246,6 +10294,9 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -11374,8 +11425,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -11717,6 +11768,18 @@ packages: utf-8-validate: optional: true + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -12771,9 +12834,15 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.26.8': {} - '@babel/compat-data@7.28.0': {} + '@babel/compat-data@7.29.0': {} '@babel/core@7.26.9': dependencies: @@ -12798,17 +12867,17 @@ snapshots: '@babel/core@7.28.0': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.0) - '@babel/helpers': 7.28.3 - '@babel/parser': 7.28.3 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -12831,12 +12900,12 @@ snapshots: '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 - '@babel/generator@7.28.3': + '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.1': @@ -12851,11 +12920,11 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-compilation-targets@7.27.2': + '@babel/helper-compilation-targets@7.28.6': dependencies: - '@babel/compat-data': 7.28.0 + '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.4 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -12895,6 +12964,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.9)': dependencies: '@babel/core': 7.26.9 @@ -12913,12 +12989,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.0)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -12952,6 +13028,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.25.9': {} '@babel/helper-validator-option@7.27.1': {} @@ -12961,10 +13039,10 @@ snapshots: '@babel/template': 7.26.9 '@babel/types': 7.26.9 - '@babel/helpers@7.28.3': + '@babel/helpers@7.28.6': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@babel/parser@7.26.9': dependencies: @@ -12974,9 +13052,9 @@ snapshots: dependencies: '@babel/types': 7.27.1 - '@babel/parser@7.28.3': + '@babel/parser@7.29.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.29.0 '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.9)': dependencies: @@ -13107,6 +13185,12 @@ snapshots: '@babel/parser': 7.27.2 '@babel/types': 7.27.1 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@babel/traverse@7.26.9': dependencies: '@babel/code-frame': 7.26.2 @@ -13131,15 +13215,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.3': + '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - debug: 4.4.1 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -13153,10 +13237,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/types@7.28.2': + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@bcherny/json-schema-ref-parser@10.0.5-fork': dependencies: @@ -13995,6 +14079,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -14759,20 +14848,18 @@ snapshots: dependencies: '@types/node': 22.16.4 - '@slack/types@2.13.1': {} + '@slack/types@2.20.0': {} - '@slack/types@2.8.0': {} - - '@slack/web-api@6.8.1': + '@slack/web-api@6.13.0': dependencies: '@slack/logger': 3.0.0 - '@slack/types': 2.8.0 + '@slack/types': 2.20.0 '@types/is-stream': 1.1.0 '@types/node': 22.16.4 - axios: 0.27.2 + axios: 1.13.6 eventemitter3: 3.1.2 form-data: 2.5.1 - is-electron: 2.2.0 + is-electron: 2.2.2 is-stream: 1.1.0 p-queue: 6.6.2 p-retry: 4.6.2 @@ -16273,6 +16360,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.2.5: dependencies: follow-redirects: 1.15.6(debug@4.4.1) @@ -16457,6 +16552,8 @@ snapshots: base64url@3.0.1: {} + baseline-browser-mapping@2.10.0: {} + bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 @@ -16628,12 +16725,13 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) - browserslist@4.25.4: + browserslist@4.28.1: dependencies: - caniuse-lite: 1.0.30001739 - electron-to-chromium: 1.5.214 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.4) + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001774 + electron-to-chromium: 1.5.302 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) bs-logger@0.2.6: dependencies: @@ -16745,7 +16843,7 @@ snapshots: caniuse-lite@1.0.30001700: {} - caniuse-lite@1.0.30001739: {} + caniuse-lite@1.0.30001774: {} capital-case@1.0.4: dependencies: @@ -17043,6 +17141,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize@5.0.1: {} decimal.js@10.6.0: @@ -17247,7 +17349,7 @@ snapshots: electron-to-chromium@1.5.104: {} - electron-to-chromium@1.5.214: {} + electron-to-chromium@1.5.302: {} emittery@0.13.1: {} @@ -17973,6 +18075,8 @@ snapshots: fn.name@1.1.0: {} + follow-redirects@1.15.11: {} + follow-redirects@1.15.2: {} follow-redirects@1.15.5: {} @@ -18035,6 +18139,14 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -18693,7 +18805,7 @@ snapshots: is-docker@3.0.0: {} - is-electron@2.2.0: {} + is-electron@2.2.2: {} is-extglob@2.1.1: {} @@ -19221,7 +19333,7 @@ snapshots: cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.6.0 - form-data: 4.0.4 + form-data: 4.0.5 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -19237,7 +19349,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.2 + ws: 8.19.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -20298,6 +20410,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.27: {} + nodemailer@6.9.3: {} nopt@7.2.1: @@ -20769,6 +20883,11 @@ snapshots: pseudomap@1.0.2: {} + psl@1.15.0: + dependencies: + punycode: 2.3.1 + optional: true + psl@1.9.0: {} pump@3.0.0: @@ -21572,7 +21691,7 @@ snapshots: cookiejar: 2.1.4 debug: 4.4.1 fast-safe-stringify: 2.1.1 - form-data: 4.0.4 + form-data: 4.0.5 formidable: 2.1.2 methods: 1.1.2 mime: 2.6.0 @@ -21713,7 +21832,7 @@ snapshots: tough-cookie@4.1.4: dependencies: - psl: 1.9.0 + psl: 1.15.0 punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 @@ -22104,9 +22223,9 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.4): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: - browserslist: 4.25.4 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -22484,6 +22603,9 @@ snapshots: ws@8.18.2: {} + ws@8.19.0: + optional: true + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0