diff --git a/.github/workflows/deploy-integrations-production.yml b/.github/workflows/deploy-integrations-production.yml index 59d3d0e423a..7b5b2c3b829 100644 --- a/.github/workflows/deploy-integrations-production.yml +++ b/.github/workflows/deploy-integrations-production.yml @@ -31,6 +31,7 @@ jobs: uses: ./.github/actions/deploy-integrations with: environment: 'production' + extra_filter: "-F '!docusign'" force: ${{ github.event.inputs.force == 'true' }} sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }} token_cloud_ops_account: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }} diff --git a/CODEOWNERS b/CODEOWNERS index 21e2393f1e3..e2949d44a0e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -37,6 +37,7 @@ /integrations/clickup @JustinBordage @botpress/engineering /integrations/confluence @Nathaniel-Girard @botpress/engineering /integrations/dropbox @pascal-botpress @botpress/engineering +/integrations/docusign @JustinBordage @botpress/engineering /integrations/email @Nathaniel-Girard @botpress/engineering /integrations/freshchat @davidvitora @botpress/engineering /integrations/github @JustinBordage @pascal-botpress @botpress/engineering diff --git a/bots/hit-looper/bot.definition.ts b/bots/hit-looper/bot.definition.ts index 5e5c4d26147..6743b7af7ca 100644 --- a/bots/hit-looper/bot.definition.ts +++ b/bots/hit-looper/bot.definition.ts @@ -38,8 +38,8 @@ export default new sdk.BotDefinition({ }) .addPlugin(hitl, { configuration: { - useHumanAgentInfo: false, flowOnHitlStopped: false, + useHumanAgentInfo: false, }, interfaces: { hitl: { diff --git a/integrations/docusign/definitions/actions.ts b/integrations/docusign/definitions/actions.ts new file mode 100644 index 00000000000..0d517456d5c --- /dev/null +++ b/integrations/docusign/definitions/actions.ts @@ -0,0 +1,42 @@ +import { z } from '@botpress/sdk' + +const _templateRecipientSchema = z.object({ + name: z.string().title('Recipient Name').describe("The recipient's full name"), + email: z.string().title('Recipient Email').describe("The recipient's email address"), + role: z.string().title('Template Recipient Role').describe('The role keyword defined in the template'), + accessCode: z + .string() + .optional() + .title('Access Code') + .describe('An access code that is required to access the envelope'), +}) +export type TemplateRecipient = z.infer + +export const sendEnvelopeInputSchema = z.object({ + templateId: z.string().title('Template ID').describe('The id of the envelope template'), + recipients: z + .array(_templateRecipientSchema) + .min(1) + .title('Envelope Recipients') + .describe( + "The recipients of the envelope to send as defined in the template (Note: adding additional recipients with roles not defined in the template will cause them to default as 'signers')" + ), + emailSubject: z + .string() + .optional() + .title('Email Subject') + .describe( + 'Sets the subject field of the sent envelope email (Leaving this empty will fallback to the template default subject)' + ), + conversationId: z + .string() + .placeholder('{{ event.conversationId }}') + .optional() + .title('Conversation ID') + .describe('The ID of the conversation'), +}) +export type SendEnvelopeInput = z.infer + +export const sendEnvelopeOutputSchema = z.object({ + envelopeId: z.string().title('Envelope ID').describe('The id of the sent envelope'), +}) diff --git a/integrations/docusign/definitions/events.ts b/integrations/docusign/definitions/events.ts new file mode 100644 index 00000000000..5f7c50388e0 --- /dev/null +++ b/integrations/docusign/definitions/events.ts @@ -0,0 +1,11 @@ +import { z } from '@botpress/sdk' + +export const envelopeEventSchema = z.object({ + userId: z.string().title('User ID').describe("The Docusign user's ID"), + accountId: z + .string() + .title('API Account ID') + .describe('The docusign user\'s "API Account ID" (This is a GUID that is found in "Apps & Keys")'), + envelopeId: z.string().title('Envelope ID').describe('The id of the sent envelope'), + triggeredAt: z.string().datetime().title('Triggered At').describe('The datetime when the event was triggered'), +}) diff --git a/integrations/docusign/eslint.config.mjs b/integrations/docusign/eslint.config.mjs new file mode 100644 index 00000000000..8c81907e7de --- /dev/null +++ b/integrations/docusign/eslint.config.mjs @@ -0,0 +1,13 @@ +import rootConfig from '../../eslint.config.mjs' + +export default [ + ...rootConfig, + { + languageOptions: { + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +] diff --git a/integrations/docusign/hub.md b/integrations/docusign/hub.md new file mode 100644 index 00000000000..545d372293b --- /dev/null +++ b/integrations/docusign/hub.md @@ -0,0 +1,43 @@ +# Integrate Docusign with AI + +Unlock the full potential of Docusign by integrating it with AI technologies. With AI, you can automate document workflows, generate intelligent insights, enhance security measures, and improve user experience. From streamlining contract management with AI-powered analytics to using machine learning for data extraction, the possibilities are endless. + +# What You Can Do with a Docusign AI Integration + +By integrating Docusign with AI-driven tools, you can unlock new possibilities to enhance document processes, security, and user interaction. Here are some key features you can leverage: + +## 1. Automate Document Workflows + +With AI, routine document workflows such as signing, sending reminders, and tracking status can be automated within Docusign, saving time and reducing manual effort. + +## 2. AI-Powered Analytics + +Integrate Docusign with AI analytics tools to monitor document performance, gain insights from contract data, and optimize workflow efficiency for better decision-making. + +## 3. Intelligent Data Extraction + +Leverage machine learning to automatically extract key information from documents, reducing errors and speeding up data processing. + +## 4. Enhanced Security Measures + +By integrating AI-driven security features, you can enhance the protection of sensitive documents, detect fraudulent activities, and ensure compliance with industry standards. + +# Benefits of Integrating Docusign with AI + +By integrating AI into Docusign, your organization can: + +- **Automate document handling:** Use AI to automate the preparation and distribution of documents, allowing your team to focus on more strategic tasks. +- **Generate insights:** Leverage AI to analyze document data and generate actionable insights for business strategy and planning. +- **Real-time language processing:** Instantly process and translate document content into multiple languages, facilitating global operations. +- **Fraud detection:** Utilize AI to identify unusual patterns and prevent fraudulent activities in document transactions. +- **Workflow optimization:** Integrate AI-powered workflow management to automatically assign, update, and track document tasks based on context and priority. + +# What is Docusign? + +Docusign is a leading electronic signature and agreement platform that allows businesses to prepare, sign, act on, and manage agreements in a secure digital environment. By integrating AI into Docusign, you can enhance its capabilities with advanced automation, security, and data-driven insights. Related Integrations: + +- Salesforce AI Integration +- Gmail AI Integration +- Zapier AI Integration +- PDF Generator AI Integration +- HubSpot AI Integration diff --git a/integrations/docusign/icon.svg b/integrations/docusign/icon.svg new file mode 100644 index 00000000000..1228dcd84f5 --- /dev/null +++ b/integrations/docusign/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/integrations/docusign/integration.definition.ts b/integrations/docusign/integration.definition.ts new file mode 100644 index 00000000000..ab5f42ed1fe --- /dev/null +++ b/integrations/docusign/integration.definition.ts @@ -0,0 +1,123 @@ +import { IntegrationDefinition, z } from '@botpress/sdk' +import { sendEnvelopeInputSchema, sendEnvelopeOutputSchema } from 'definitions/actions' +import { envelopeEventSchema } from 'definitions/events' + +export default new IntegrationDefinition({ + name: 'docusign', + title: 'Docusign', + version: '2.0.1', + readme: 'hub.md', + icon: 'icon.svg', + description: + 'Automate document workflows, generate intelligent insights, enhance security measures, and improve user experience.', + configuration: { + identifier: { + linkTemplateScript: 'linkTemplate.vrl', + }, + schema: z.object({ + accountId: z + .string() + .optional() + .title('API Account ID (Optional)') + .describe( + 'The docusign user\'s "API Account ID" (This is a GUID that is found in "Apps & Keys")\nThe default account will be selected if left empty' + ) + .placeholder('e.g. a1b2c3d4-e5f6-g7h8-i9j0-d4c3b2a1'), + }), + }, + actions: { + sendEnvelope: { + title: 'Send Envelope', + description: 'Sends an envelope (document) to a recipient to sign it', + input: { + schema: sendEnvelopeInputSchema, + }, + output: { + schema: sendEnvelopeOutputSchema, + }, + }, + }, + events: { + envelopeSent: { + title: 'Envelope Sent', + description: 'An event that triggers when an envelope is sent to the recipient(s) to be signed', + schema: envelopeEventSchema, + }, + envelopeResent: { + title: 'Envelope Resent', + description: 'An event that triggers when an envelope is resent to the recipient(s) via the dashboard', + schema: envelopeEventSchema, + }, + envelopeCompleted: { + title: 'Envelope Completed', + description: 'An event that triggers when an envelope has been completed/signed by all recipient(s)', + schema: envelopeEventSchema, + }, + envelopeDeclined: { + title: 'Envelope Declined', + description: 'An event that triggers when a recipient has declined to sign an envelope', + schema: envelopeEventSchema, + }, + envelopeVoided: { + title: 'Envelope Voided', + description: 'An event that triggers when an envelope has been voided by the sender', + schema: envelopeEventSchema, + }, + }, + secrets: { + OAUTH_BASE_URL: { + description: 'The base URL used for OAuth authentication', + }, + OAUTH_CLIENT_ID: { + description: "The unique identifier that's used to initiate the OAuth flow", + }, + OAUTH_CLIENT_SECRET: { + description: "A secret that's used to establish and refresh the OAuth authentication", + }, + }, + states: { + configuration: { + type: 'integration', + schema: z.object({ + oauth: z + .object({ + refreshToken: z.string().describe('The refresh token for the integration').title('Refresh Token'), + accessToken: z.string().describe('The access token for the integration').title('Access Token'), + tokenType: z + .string() + .describe('The authentication header type for the access token (e.g. "Bearer")') + .title('Token Type'), + expiresAt: z + .number() + .min(0) + .describe('The expiry time of the access token represented as a Unix timestamp (milliseconds)') + .title('Expires At'), + }) + .describe('The parameters used for accessing the Docusign API and refreshing the access token') + .title('OAuth Parameters') + .nullable(), + }), + }, + account: { + type: 'integration', + schema: z.object({ + account: z + .object({ + id: z.string().title('API Account ID').describe("The docusign user's api account id"), + baseUri: z.string().describe('The base URI for the Docusign API').title('Base URI'), + refreshAt: z + .number() + .min(0) + .title('Refresh At') + .describe( + 'The unix timestamp (milliseconds) that the selected account will be refreshed (Only when not explicitly selected in the config)' + ) + .nullable(), + }) + .title('Account Info') + .describe("The docusign account's info") + .nullable(), + }), + }, + }, +}) diff --git a/integrations/docusign/linkTemplate.vrl b/integrations/docusign/linkTemplate.vrl new file mode 100644 index 00000000000..fa4dc8095a0 --- /dev/null +++ b/integrations/docusign/linkTemplate.vrl @@ -0,0 +1,13 @@ +webhookId = to_string!(.webhookId) +webhookUrl = to_string!(.webhookUrl) +env = to_string!(.env) + +baseDomain = "https://account-d.docusign.com/oauth/auth" +clientId = "785488f7-c2a8-4b9f-b9ca-6a94b3dd317c" + +if env == "production" { + baseDomain = "https://account.docusign.com/oauth/auth" + clientId = "c32faf9a-a4c7-43dc-9b3a-3e1e902c40ec" +} + +"{{baseDomain}}?client_id={{ clientId }}&response_type=code&scope=signature&state={{ webhookId }}&redirect_uri={{ webhookUrl }}/oauth" \ No newline at end of file diff --git a/integrations/docusign/package.json b/integrations/docusign/package.json new file mode 100644 index 00000000000..5b5b3e157a6 --- /dev/null +++ b/integrations/docusign/package.json @@ -0,0 +1,18 @@ +{ + "name": "@botpresshub/docusign", + "scripts": { + "build": "bp add -y && bp build", + "check:type": "tsc --noEmit", + "check:bplint": "bp lint" + }, + "private": true, + "dependencies": { + "@botpress/client": "workspace:*", + "@botpress/sdk": "workspace:*", + "axios": "^1.12.2", + "docusign-esign": "^8.4.0" + }, + "devDependencies": { + "@types/docusign-esign": "^5.19.9" + } +} diff --git a/integrations/docusign/src/actions/index.ts b/integrations/docusign/src/actions/index.ts new file mode 100644 index 00000000000..426654322e4 --- /dev/null +++ b/integrations/docusign/src/actions/index.ts @@ -0,0 +1,6 @@ +import { sendEnvelope } from './send-envelope' +import * as bp from '.botpress' + +export default { + sendEnvelope, +} satisfies bp.IntegrationProps['actions'] diff --git a/integrations/docusign/src/actions/send-envelope.ts b/integrations/docusign/src/actions/send-envelope.ts new file mode 100644 index 00000000000..806de89abbb --- /dev/null +++ b/integrations/docusign/src/actions/send-envelope.ts @@ -0,0 +1,14 @@ +import { DocusignClient } from '../docusign-api' +import { sendEnvelopeInputToEnvelopeDefinition } from '../docusign-api/helpers' +import * as bp from '.botpress' + +export const sendEnvelope: bp.IntegrationProps['actions']['sendEnvelope'] = async ({ input, ...props }) => { + const envelopeDef = sendEnvelopeInputToEnvelopeDefinition(input) + + const apiClient = await DocusignClient.create(props) + const resp = await apiClient.sendEnvelope(envelopeDef) + + return { + envelopeId: resp.envelopeId, + } +} diff --git a/integrations/docusign/src/config.ts b/integrations/docusign/src/config.ts new file mode 100644 index 00000000000..4d983185337 --- /dev/null +++ b/integrations/docusign/src/config.ts @@ -0,0 +1 @@ +export const CONVERSATION_ID_FIELD_KEY = 'Botpress-Conversation-ID' diff --git a/integrations/docusign/src/docusign-api/auth-utils.ts b/integrations/docusign/src/docusign-api/auth-utils.ts new file mode 100644 index 00000000000..ab1e9577647 --- /dev/null +++ b/integrations/docusign/src/docusign-api/auth-utils.ts @@ -0,0 +1,151 @@ +import { RuntimeError } from '@botpress/sdk' +import { CommonHandlerProps } from '../types' +import { DocusignAuthClient } from './auth' +import { GetAccessTokenResp, UserAccount } from './schemas' +import * as bp from '.botpress' + +export const MS_PER_MINUTE = 60000 +export const MS_PER_HOUR = MS_PER_MINUTE * 60 + +export const applyOAuthState = async ({ client, ctx }: CommonHandlerProps, tokenResp: GetAccessTokenResp) => { + const { accessToken, refreshToken, expiresAt, tokenType } = tokenResp + + const { state } = await client.setState({ + type: 'integration', + name: 'configuration', + id: ctx.integrationId, + payload: { + oauth: { + accessToken, + tokenType, + refreshToken, + expiresAt, + }, + }, + }) + + if (!state.payload.oauth) { + throw new Error('Failed to store OAuth state') + } + + return state.payload.oauth +} + +const OAUTH_TIMEOUT_BUFFER = MS_PER_MINUTE * 5 +export const getOAuthState = async (props: CommonHandlerProps, authClient?: DocusignAuthClient) => { + const { state } = await props.client.getOrSetState({ + type: 'integration', + name: 'configuration', + id: props.ctx.integrationId, + payload: { + oauth: null, + }, + }) + let oauthState = state.payload.oauth + + if (!oauthState) { + throw new RuntimeError('User authentication has not been completed') + } + + const { expiresAt, refreshToken } = oauthState + if (expiresAt - OAUTH_TIMEOUT_BUFFER <= Date.now()) { + authClient ??= new DocusignAuthClient() + const tokenResp = await authClient.getAccessTokenWithRefreshToken(refreshToken) + if (!tokenResp.success) throw tokenResp.error + + oauthState = await applyOAuthState(props, tokenResp.data) + } + + return oauthState +} + +const _findAccount = (accountsList: UserAccount[], explicitAccountId: string | undefined): UserAccount => { + let account: UserAccount | null = null + + if (explicitAccountId) { + account = accountsList.find(({ account_id }) => account_id === explicitAccountId) ?? null + + if (!account) { + throw new RuntimeError('An account with the specified API Account ID does not exist or is not owned by this user') + } + } else { + account = accountsList.find(({ is_default }: UserAccount) => is_default) ?? accountsList[0]! + } + + return account +} + +const ACCOUNT_REFRESH_AFTER = MS_PER_HOUR * 24 +export const refreshAccountState = async (props: CommonHandlerProps) => { + const authClient = new DocusignAuthClient() + + const { accessToken, tokenType } = await getOAuthState(props, authClient) + + const userInfoResp = await authClient.getUserInfo(accessToken, tokenType) + if (!userInfoResp.success) throw userInfoResp.error + + const { client, ctx } = props + const { accounts } = userInfoResp.data + + const explicitAccountId = ctx.configuration.accountId + const account = _findAccount(accounts, explicitAccountId) + + const refreshAt = !explicitAccountId?.trim() ? Date.now() + ACCOUNT_REFRESH_AFTER : null + const { state } = await client.setState({ + type: 'integration', + name: 'account', + id: ctx.integrationId, + payload: { + account: { + id: account.account_id, + baseUri: account.base_uri, + refreshAt, + }, + }, + }) + + if (!state.payload.account) { + throw new RuntimeError('Failed to store account state') + } + + return state.payload.account +} + +export const getAccountState = async (props: CommonHandlerProps) => { + const { state } = await props.client.getOrSetState({ + type: 'integration', + name: 'account', + id: props.ctx.integrationId, + payload: { + account: null, + }, + }) + + let hasAccountChanged = false + let accountState = state.payload.account + if (!accountState || (accountState.refreshAt && accountState.refreshAt <= Date.now())) { + const prevAccountState = accountState + accountState = await refreshAccountState(props) + + if (accountState.id !== prevAccountState?.id) { + hasAccountChanged = true + } + } + + return { account: accountState, hasChanged: hasAccountChanged } +} + +export const exchangeAuthCodeForRefreshToken = async (props: bp.HandlerProps, oAuthCode: string): Promise => { + const authClient = new DocusignAuthClient() + const tokenResp = await authClient.getAccessTokenWithCode(oAuthCode) + if (!tokenResp.success) throw tokenResp.error + + const userInfoResp = await authClient.getUserInfo(tokenResp.data.accessToken, tokenResp.data.tokenType) + if (!userInfoResp.success) throw userInfoResp.error + + await applyOAuthState(props, tokenResp.data) + + await props.client.configureIntegration({ + identifier: userInfoResp.data.sub, + }) +} diff --git a/integrations/docusign/src/docusign-api/auth.ts b/integrations/docusign/src/docusign-api/auth.ts new file mode 100644 index 00000000000..3cb34a9dd90 --- /dev/null +++ b/integrations/docusign/src/docusign-api/auth.ts @@ -0,0 +1,109 @@ +import { RuntimeError } from '@botpress/sdk' +import axios, { type AxiosInstance } from 'axios' +import type { Result } from '../types' +import { + type GetAccessTokenResp, + docusignOAuthAccessTokenRespSchema, + type GetUserInfoResp, + getUserInfoRespSchema, +} from './schemas' +import { GetAccessTokenParams } from './types' +import * as bp from '.botpress' + +export class DocusignAuthClient { + private _axiosClient: AxiosInstance + + public constructor() { + const { OAUTH_BASE_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET } = bp.secrets + + // Opted for axios here since the docusign package only has + // a function for getting an accessToken from the oauth code + // but not for refresh tokens + this._axiosClient = axios.create({ + baseURL: OAUTH_BASE_URL, + headers: { + Authorization: `Basic ${Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`).toString('base64')}`, + 'Cache-Control': 'no-store', + Pragma: 'no-cache', + }, + }) + } + + private async _getAccessToken(params: GetAccessTokenParams): Promise> { + // The Docusign API Postman collection example uses FormData for this endpoint. + const formData = new FormData() + Object.entries(params).forEach(([key, value]) => formData.append(key, value)) + + // Docusign doesn't return a timestamp when a token is issued. So a timestamp + // is generated prior to the request being made so the expiry time is accurate. + const tokenRequestedAt = Date.now() + + const resp = await this._axiosClient.post('/oauth/token', formData) + + if (resp.status < 200 || resp.status >= 300) { + return { + success: false, + error: new RuntimeError( + `Failed to retrieve access token w/${params.grant_type} | Invalid Status '${resp.status}'` + ), + } + } + + const result = docusignOAuthAccessTokenRespSchema.safeParse(resp.data) + if (!result.success) { + return { + success: false, + error: new RuntimeError(`Failed to retrieve access token w/${params.grant_type} | Schema Parse Failure`), + } + } + + return { + success: true, + data: { + accessToken: result.data.access_token, + tokenType: result.data.token_type, + expiresAt: tokenRequestedAt + result.data.expires_in * 1000, + refreshToken: result.data.refresh_token, + }, + } + } + + public async getAccessTokenWithCode(code: string): Promise> { + return this._getAccessToken({ + grant_type: 'authorization_code', + code, + }) + } + + public async getAccessTokenWithRefreshToken(refreshToken: string): Promise> { + return this._getAccessToken({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }) + } + + public async getUserInfo(accessToken: string, tokenType: string): Promise> { + const resp = await this._axiosClient.get('/oauth/userinfo', { + headers: { + Authorization: `${tokenType} ${accessToken}`, + }, + }) + + if (resp.status < 200 || resp.status >= 300) { + return { + success: false, + error: new RuntimeError(`Failed to retrieve user info | Invalid Status '${resp.status}'`), + } + } + + const result = getUserInfoRespSchema.safeParse(resp.data) + if (!result.success) { + return { + success: false, + error: new RuntimeError('Failed to retrieve user info | Schema Parse Failure'), + } + } + + return { success: true, data: result.data } + } +} diff --git a/integrations/docusign/src/docusign-api/helpers.ts b/integrations/docusign/src/docusign-api/helpers.ts new file mode 100644 index 00000000000..8c0e8691b6e --- /dev/null +++ b/integrations/docusign/src/docusign-api/helpers.ts @@ -0,0 +1,33 @@ +import { TemplateRecipient, SendEnvelopeInput } from 'definitions/actions' +import docusign from 'docusign-esign' +import { CONVERSATION_ID_FIELD_KEY } from '../config' + +const _createTemplateRecipient = (recipientInfo: TemplateRecipient): docusign.TemplateRole => { + const { email, name, role, accessCode } = recipientInfo + return { + email, + name, + roleName: role, + accessCode, + } +} + +export const sendEnvelopeInputToEnvelopeDefinition = (input: SendEnvelopeInput): docusign.EnvelopeDefinition => { + const { emailSubject, templateId, recipients, conversationId } = input + + return { + emailSubject, + templateId, + templateRoles: recipients.map(_createTemplateRecipient), + status: 'sent', + customFields: { + textCustomFields: [ + { + name: CONVERSATION_ID_FIELD_KEY, + value: conversationId, + show: 'false', + }, + ], + }, + } +} diff --git a/integrations/docusign/src/docusign-api/index.ts b/integrations/docusign/src/docusign-api/index.ts new file mode 100644 index 00000000000..8f8a69b2321 --- /dev/null +++ b/integrations/docusign/src/docusign-api/index.ts @@ -0,0 +1,111 @@ +import { RuntimeError } from '@botpress/sdk' +import axios, { AxiosInstance } from 'axios' +import docusign from 'docusign-esign' +import type { CommonHandlerProps } from '../types' +import { getAccountState, getOAuthState } from './auth-utils' +import { constructWebhookBody, refreshWebhooks } from './utils' + +type DocusignClientParams = { + accountId: string + baseUri: string + accessToken: string + tokenType: string +} + +export class DocusignClient { + private _axiosClient: AxiosInstance + private _accountId: string + + private constructor(params: DocusignClientParams) { + const { baseUri, tokenType, accessToken, accountId } = params + this._accountId = accountId + + this._axiosClient = axios.create({ + baseURL: `${baseUri}/restapi/v2.1`, + headers: { + Authorization: `${tokenType} ${accessToken}`, + }, + }) + } + + public async sendEnvelope(envelope: docusign.EnvelopeDefinition) { + try { + const resp = await this._axiosClient.post( + `/accounts/${this._accountId}/envelopes`, + envelope + ) + const { data } = resp + + if (!data.envelopeId) { + const message = data.errorDetails ? data.errorDetails.message : 'Did not receive EnvelopeID from Docusign' + throw new Error(message) + } + + return { + envelopeId: data.envelopeId, + } + } catch (thrown: unknown) { + const err = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new RuntimeError('Failed to send envelope', err) + } + } + + public async listWebhooks(): Promise { + const resp = await this._axiosClient.get(`/accounts/${this._accountId}/connect`) + return resp.data.configurations + } + + public async createWebhook(webhookUrl: string, botId: string): Promise { + try { + const body = constructWebhookBody(webhookUrl, botId) + const resp = await this._axiosClient.post( + `/accounts/${this._accountId}/connect`, + body + ) + + const { connectId } = resp.data + if (!connectId) { + throw new RuntimeError('Failed to create webhook due to unexpected api response') + } + return connectId + } catch (thrown: unknown) { + if (thrown instanceof RuntimeError) { + throw thrown + } + + const error = + thrown instanceof Error ? new RuntimeError(thrown.message, thrown) : new RuntimeError(String(thrown)) + throw error + } + } + + public async removeWebhook(connectId: string): Promise { + try { + await this._axiosClient.delete(`/accounts/${this._accountId}/connect/${connectId}`) + return true + } catch { + return false + } + } + + /** Creates a docusign api client from the oauth parameters */ + public static async create(props: CommonHandlerProps): Promise { + const [oauthState, accountState] = await Promise.all([getOAuthState(props), getAccountState(props)]) + const { account, hasChanged: hasAccountChanged } = accountState + + const apiClient = new DocusignClient({ + accountId: account.id, + baseUri: account.baseUri, + accessToken: oauthState.accessToken, + tokenType: oauthState.tokenType, + }) + + // This side-effect doesn't really belong here, but I can't put + // it anywhere else without it leading to fragile behaviour. + if (hasAccountChanged) { + await refreshWebhooks(props, process.env.BP_WEBHOOK_URL!, apiClient) + } + + return apiClient + } +} diff --git a/integrations/docusign/src/docusign-api/schemas.ts b/integrations/docusign/src/docusign-api/schemas.ts new file mode 100644 index 00000000000..b012160f43f --- /dev/null +++ b/integrations/docusign/src/docusign-api/schemas.ts @@ -0,0 +1,36 @@ +import { z } from '@botpress/sdk' + +export const docusignOAuthAccessTokenRespSchema = z.object({ + access_token: z.string(), + token_type: z.string().describe('The authentication header type (e.g. "Bearer")'), + expires_in: z.number().describe('Seconds until the token expires'), + refresh_token: z.string(), + scope: z.string(), +}) + +export type GetAccessTokenResp = { + accessToken: string + /** The authentication header type for the access token (e.g. "Bearer") */ + tokenType: string + /** The expiry time of the access token represented as a Unix timestamp (milliseconds) */ + expiresAt: number + refreshToken: string +} + +const _userAccountSchema = z + .object({ + account_id: z.string(), + account_name: z.string(), + is_default: z.boolean(), + base_uri: z.string().url(), + }) + .strip() +export type UserAccount = z.infer + +export const getUserInfoRespSchema = z + .object({ + sub: z.string().min(1), + accounts: z.array(_userAccountSchema).min(1), + }) + .strip() +export type GetUserInfoResp = z.infer diff --git a/integrations/docusign/src/docusign-api/types.ts b/integrations/docusign/src/docusign-api/types.ts new file mode 100644 index 00000000000..9833a6379f8 --- /dev/null +++ b/integrations/docusign/src/docusign-api/types.ts @@ -0,0 +1,9 @@ +export type GetAccessTokenParams = + | { + grant_type: 'authorization_code' + code: string + } + | { + grant_type: 'refresh_token' + refresh_token: string + } diff --git a/integrations/docusign/src/docusign-api/utils.ts b/integrations/docusign/src/docusign-api/utils.ts new file mode 100644 index 00000000000..66e07d9a548 --- /dev/null +++ b/integrations/docusign/src/docusign-api/utils.ts @@ -0,0 +1,53 @@ +import { CommonHandlerProps } from '../types' +import { DocusignClient } from '.' + +export const constructWebhookBody = (webhookUrl: string, botId: string, additionalProps: object = {}) => { + return { + configurationType: 'custom', + urlToPublishTo: webhookUrl, + name: `Botpress Integration | Bot ID: ${botId}`, + allowEnvelopePublish: 'true', + enableLog: 'true', + deliveryMode: 'SIM', + requiresAcknowledgement: 'true', + signMessageWithX509Certificate: 'true', + includeTimeZoneInformation: 'true', + includeHMAC: 'true', + includeEnvelopeVoidReason: 'false', + includeSenderAccountasCustomField: 'true', + integratorManaged: 'true', + envelopeEvents: ['Sent', 'Delivered', 'Completed', 'Declined', 'Voided'], + events: ['envelope-resent', 'envelope-reminder-sent'], + allUsers: 'true', + eventData: { + version: 'restv2.1', + includeData: ['custom_fields'], + }, + ...additionalProps, + } +} + +export const cleanupWebhooks = async (props: CommonHandlerProps, webhookUrl: string, apiClient?: DocusignClient) => { + apiClient ??= await DocusignClient.create(props) + + const resp = await apiClient.listWebhooks() + const webhookDeletionPromises = resp?.reduce((webhookPromises, configuration) => { + const { urlToPublishTo, connectId } = configuration + if (!connectId || urlToPublishTo !== webhookUrl) { + return webhookPromises + } + + const promise = apiClient.removeWebhook(connectId) + return webhookPromises.concat(promise) + }, [] as Promise[]) + + await Promise.allSettled(webhookDeletionPromises ?? []) +} + +export const refreshWebhooks = async (props: CommonHandlerProps, webhookUrl: string, apiClient?: DocusignClient) => { + apiClient ??= await DocusignClient.create(props) + + await cleanupWebhooks(props, webhookUrl, apiClient) + + await apiClient.createWebhook(webhookUrl, props.ctx.botId) +} diff --git a/integrations/docusign/src/handler.ts b/integrations/docusign/src/handler.ts new file mode 100644 index 00000000000..2779dbeebdb --- /dev/null +++ b/integrations/docusign/src/handler.ts @@ -0,0 +1,24 @@ +import { exchangeAuthCodeForRefreshToken } from './docusign-api/auth-utils' +import { dispatchIntegrationEvent } from './webhooks/event-dispatcher' +import { parseWebhookEvent } from './webhooks/utils' +import * as bp from '.botpress' + +const _isOauthRequest = ({ req }: bp.HandlerProps) => req.path === '/oauth' + +export const handler = async (props: bp.HandlerProps) => { + if (_isOauthRequest(props)) { + const oAuthCode = new URLSearchParams(props.req.query).get('code') + if (oAuthCode === null) throw new Error('Missing authentication code') + + await exchangeAuthCodeForRefreshToken(props, oAuthCode) + return + } + + const result = parseWebhookEvent(props) + if (!result.success) { + props.logger.forBot().error(result.error.message, result.error) + return + } + + await dispatchIntegrationEvent(props, result.data) +} diff --git a/integrations/docusign/src/index.ts b/integrations/docusign/src/index.ts new file mode 100644 index 00000000000..6a181556439 --- /dev/null +++ b/integrations/docusign/src/index.ts @@ -0,0 +1,12 @@ +import actions from './actions' +import { handler } from './handler' +import { register, unregister } from './setup' +import * as bp from '.botpress' + +export default new bp.Integration({ + register, + unregister, + actions, + channels: {}, + handler, +}) diff --git a/integrations/docusign/src/setup.ts b/integrations/docusign/src/setup.ts new file mode 100644 index 00000000000..94bcdfa69fe --- /dev/null +++ b/integrations/docusign/src/setup.ts @@ -0,0 +1,12 @@ +import { refreshAccountState } from './docusign-api/auth-utils' +import { cleanupWebhooks, refreshWebhooks } from './docusign-api/utils' +import * as bp from '.botpress' + +export const register: bp.Integration['register'] = async (props) => { + await refreshAccountState(props) + await refreshWebhooks(props, props.webhookUrl) +} + +export const unregister: bp.Integration['unregister'] = async (props) => { + await cleanupWebhooks(props, props.webhookUrl) +} diff --git a/integrations/docusign/src/types.ts b/integrations/docusign/src/types.ts new file mode 100644 index 00000000000..219df76ab38 --- /dev/null +++ b/integrations/docusign/src/types.ts @@ -0,0 +1,9 @@ +import * as bp from '.botpress' + +export type Result = { success: true; data: T } | { success: false; error: Error } + +export type CommonHandlerProps = { + ctx: bp.Context + client: bp.Client + logger: bp.Logger +} diff --git a/integrations/docusign/src/utils.ts b/integrations/docusign/src/utils.ts new file mode 100644 index 00000000000..a58a154ec0b --- /dev/null +++ b/integrations/docusign/src/utils.ts @@ -0,0 +1,15 @@ +import { Result } from './types' + +export const safeParseJson = (json: string): Result => { + try { + return { + success: true, + data: JSON.parse(json), + } + } catch (thrown: unknown) { + return { + success: false, + error: thrown instanceof Error ? thrown : new Error(String(thrown)), + } + } +} diff --git a/integrations/docusign/src/webhooks/event-dispatcher.ts b/integrations/docusign/src/webhooks/event-dispatcher.ts new file mode 100644 index 00000000000..2d0e0a6d0ea --- /dev/null +++ b/integrations/docusign/src/webhooks/event-dispatcher.ts @@ -0,0 +1,21 @@ +import * as inviteeHandlers from './event-handlers' +import { AllEnvelopeEvents } from './schemas' +import * as bp from '.botpress' + +export const dispatchIntegrationEvent = async (props: bp.HandlerProps, webhookEvent: AllEnvelopeEvents) => { + switch (webhookEvent.event) { + case 'envelope-sent': + return await inviteeHandlers.handleEnvelopeEvent(props, 'envelopeSent', webhookEvent) + case 'envelope-resent': + return await inviteeHandlers.handleEnvelopeEvent(props, 'envelopeResent', webhookEvent) + case 'envelope-completed': + return await inviteeHandlers.handleEnvelopeEvent(props, 'envelopeCompleted', webhookEvent) + case 'envelope-declined': + return await inviteeHandlers.handleEnvelopeEvent(props, 'envelopeDeclined', webhookEvent) + case 'envelope-voided': + return await inviteeHandlers.handleEnvelopeEvent(props, 'envelopeVoided', webhookEvent) + default: + props.logger.warn(`Ignoring unsupported webhook type: '${webhookEvent.event}'`) + return null + } +} diff --git a/integrations/docusign/src/webhooks/event-handlers.ts b/integrations/docusign/src/webhooks/event-handlers.ts new file mode 100644 index 00000000000..34d0d7d7018 --- /dev/null +++ b/integrations/docusign/src/webhooks/event-handlers.ts @@ -0,0 +1,27 @@ +import { CONVERSATION_ID_FIELD_KEY } from '../config' +import { EnvelopeEvent } from './schemas' +import * as bp from '.botpress' + +export const handleEnvelopeEvent = async ( + props: bp.HandlerProps, + eventType: keyof bp.events.Events, + event: EnvelopeEvent +) => { + const { userId, accountId, envelopeId, envelopeSummary } = event.data + + const conversationIdField = envelopeSummary.customFields.textCustomFields.find((customField) => { + return customField.name === CONVERSATION_ID_FIELD_KEY + }) + const conversationId = conversationIdField?.value ?? undefined + + return await props.client.createEvent({ + type: eventType, + conversationId, + payload: { + userId, + accountId, + envelopeId, + triggeredAt: event.generatedDateTime.toISOString(), + }, + }) +} diff --git a/integrations/docusign/src/webhooks/schemas.ts b/integrations/docusign/src/webhooks/schemas.ts new file mode 100644 index 00000000000..ceb20b9ad13 --- /dev/null +++ b/integrations/docusign/src/webhooks/schemas.ts @@ -0,0 +1,48 @@ +import { z } from '@botpress/sdk' + +const _ignoredEnvelopeEventsSchema = z + .object({ + // Add more as necessary + event: z.literal('envelope-delivered'), // The delivered event is ignored since it's trigger is misleading + }) + .strip() + +const _customTextFieldSchema = z + .object({ + name: z.string(), + value: z.string(), + }) + .strip() + +const _envelopeEventSchema = z + .object({ + event: z.union([ + z.literal('envelope-sent'), + z.literal('envelope-resent'), + z.literal('envelope-completed'), + z.literal('envelope-declined'), + z.literal('envelope-voided'), + ]), + generatedDateTime: z.coerce.date(), + data: z + .object({ + userId: z.string(), + accountId: z.string(), + envelopeId: z.string(), + envelopeSummary: z + .object({ + customFields: z + .object({ + textCustomFields: z.array(_customTextFieldSchema), + }) + .strip(), + }) + .strip(), + }) + .strip(), + }) + .strip() +export type EnvelopeEvent = z.infer + +export const allEnvelopeEventsSchema = z.union([_envelopeEventSchema, _ignoredEnvelopeEventsSchema]) +export type AllEnvelopeEvents = z.infer diff --git a/integrations/docusign/src/webhooks/utils.ts b/integrations/docusign/src/webhooks/utils.ts new file mode 100644 index 00000000000..0dc8a750c10 --- /dev/null +++ b/integrations/docusign/src/webhooks/utils.ts @@ -0,0 +1,31 @@ +import { Result } from '../types' +import { safeParseJson } from '../utils' +import { AllEnvelopeEvents, allEnvelopeEventsSchema } from './schemas' +import * as bp from '.botpress' + +export const parseWebhookEvent = (props: bp.HandlerProps): Result => { + const { body } = props.req + if (!body?.trim()) { + return { success: false, error: new Error('Received empty webhook payload') } + } + + const parseResult = safeParseJson(body) + if (!parseResult.success) { + return { + success: false, + error: new Error(`Unable to parse Docusign Webhook Payload: ${parseResult.error.message}`), + } + } + + const zodResult = allEnvelopeEventsSchema.safeParse(parseResult.data) + if (!zodResult.success) { + const errorMsg = `Webhook handler received unexpected payload: ${zodResult.error.message}` + props.logger.error(errorMsg) + return { success: false, error: new Error(errorMsg) } + } + + return { + success: true, + data: zodResult.data, + } +} diff --git a/integrations/docusign/tsconfig.json b/integrations/docusign/tsconfig.json new file mode 100644 index 00000000000..6662da754d1 --- /dev/null +++ b/integrations/docusign/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist" + }, + "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "integration.definition.ts"] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 8d00a23ecaf..af924efe713 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "4.17.10", + "version": "4.17.11", "description": "Botpress CLI", "scripts": { "build": "pnpm run bundle && pnpm run template:gen", @@ -22,7 +22,7 @@ "@apidevtools/json-schema-ref-parser": "^11.7.0", "@botpress/chat": "0.5.1", "@botpress/client": "1.24.2", - "@botpress/sdk": "4.15.7", + "@botpress/sdk": "4.15.8", "@bpinternal/const": "^0.1.0", "@bpinternal/tunnel": "^0.1.1", "@bpinternal/yargs-extra": "^0.0.3", diff --git a/packages/cli/templates/empty-bot/package.json b/packages/cli/templates/empty-bot/package.json index e5f813cc16c..774478f1d06 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.24.2", - "@botpress/sdk": "4.15.7" + "@botpress/sdk": "4.15.8" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-integration/hub.md b/packages/cli/templates/empty-integration/hub.md index a55e8085c60..3874c6d6b8b 100644 --- a/packages/cli/templates/empty-integration/hub.md +++ b/packages/cli/templates/empty-integration/hub.md @@ -1 +1,33 @@ -# Empty Integration +# Integration Title + +> Describe the integration's purpose. + +## Configuration + +> Explain how to configure your integration and list prerequisites `ex: accounts, etc.`. +> You might also want to add configuration details for specific use cases. + +## Usage + +> Explain how to use your integration. +> You might also want to include an example if there is a specific use case. + +## Limitations + +> List the known bugs. +> List known limits `ex: rate-limiting, payload sizes, etc.` +> List unsupported use cases. + +## Changelog + +> If some versions of your integration introduce changes worth mentionning (breaking changes, bug fixes), describe them here. This will help users to know what to expect when updating the integration. + +### Integration publication checklist + +- [ ] The register handler is implemented and validates the configuration. +- [ ] Title and descriptions for all schemas are present in `integration.definition.ts`. +- [ ] Events store `conversationId`, `userId` and `messageId` when available. +- [ ] Implement events & actions that are related to `channels`, `entities`, `user`, `conversations` and `messages`. +- [ ] Events related to messages are implemented as messages. +- [ ] When an action is required by the bot developer, a `RuntimeError` is thrown with instructions to fix the problem. +- [ ] Bot name and bot avatar URL fields are available in the integration configuration. diff --git a/packages/cli/templates/empty-integration/package.json b/packages/cli/templates/empty-integration/package.json index 9c584ad16a3..0aab75e3a43 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.24.2", - "@botpress/sdk": "4.15.7" + "@botpress/sdk": "4.15.8" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-plugin/hub.md b/packages/cli/templates/empty-plugin/hub.md new file mode 100644 index 00000000000..f462e8a9a6d --- /dev/null +++ b/packages/cli/templates/empty-plugin/hub.md @@ -0,0 +1,33 @@ +# Plugin Title + +> Describe the plugin's purpose. + +## Configuration + +> Explain how to configure your plugin and list prerequisites `ex: accounts, etc.`. +> You might also want to add configuration details for specific use cases. + +## Usage + +> Explain how to use your plugin. +> You might also want to include an example if there is a specific use case. + +## Limitations + +> List the known bugs. +> List known limits `ex: rate-limiting, payload sizes, etc.` +> List unsupported use cases. + +## Changelog + +> If some versions of your plugin introduce changes worth mentionning (breaking changes, bug fixes), describe them here. This will help users to know what to expect when updating the plugin. + +### Plugin publication checklist + +- [ ] The register handler is implemented and validates the configuration. +- [ ] Title and descriptions for all schemas are present in `plugin.definition.ts`. +- [ ] Events store `conversationId`, `userId` and `messageId` when available. +- [ ] Implement events & actions that are related to `channels`, `entities`, `user`, `conversations` and `messages`. +- [ ] Events related to messages are implemented as messages. +- [ ] When an action is required by the bot developer, a `RuntimeError` is thrown with instructions to fix the problem. +- [ ] Bot name and bot avatar URL fields are available in the plugin configuration. diff --git a/packages/cli/templates/empty-plugin/package.json b/packages/cli/templates/empty-plugin/package.json index e3cbd3206f3..1730daaa8a5 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.15.7" + "@botpress/sdk": "4.15.8" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/hello-world/hub.md b/packages/cli/templates/hello-world/hub.md index 29658341f39..fec6110b085 100644 --- a/packages/cli/templates/hello-world/hub.md +++ b/packages/cli/templates/hello-world/hub.md @@ -1 +1,43 @@ # Hello World + +This integration is a template with a single action. + +> Describe the integration's purpose. + +## Configuration + +This integration does not need a configuration. + +> Explain how to configure your integration and list prerequisites `ex: accounts, etc.`. +> You might also want to add configuration details for specific use cases. + +## Usage + +To use, call the action `helloWorld`. This action will greet the user. + +> Explain how to use your integration. +> You might also want to include an example if there is a specific use case. + +## Limitations + +The `helloWorld` action has a max name size limit of 2^28 - 16 characters (the max javascript string size). + +> List the known bugs. +> List known limits `ex: rate-limiting, payload sizes, etc.` +> List unsupported use cases. + +## Changelog + +- 0.1.0: Implemented `helloWorld` action. + +> If some versions of your integration introduce changes worth mentionning (breaking changes, bug fixes), describe them here. This will help users to know what to expect when updating the integration. + +### Integration publication checklist + +- [ ] The register handler is implemented and validates the configuration. +- [ ] Title and descriptions for all schemas are present in `integration.definition.ts`. +- [ ] Events store `conversationId`, `userId` and `messageId` when available. +- [ ] Implement events & actions that are related to `channels`, `entities`, `user`, `conversations` and `messages`. +- [ ] Events related to messages are implemented as messages. +- [ ] When an action is required by the bot developer, a `RuntimeError` is thrown with instructions to fix the problem. +- [ ] Bot name and bot avatar URL fields are available in the integration configuration. diff --git a/packages/cli/templates/hello-world/package.json b/packages/cli/templates/hello-world/package.json index 9f6f47712ef..3bcd93fccec 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.24.2", - "@botpress/sdk": "4.15.7" + "@botpress/sdk": "4.15.8" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/webhook-message/hub.md b/packages/cli/templates/webhook-message/hub.md index d26009bb7eb..226853d4554 100644 --- a/packages/cli/templates/webhook-message/hub.md +++ b/packages/cli/templates/webhook-message/hub.md @@ -1 +1,51 @@ # Webhook Message + +This integration serves as a template for receiving events through a webhook and creating messages for them, as well as sending messages through an external API. + +> Describe the integration's purpose. + +## Configuration + +To use this integration, you will need to manually subscribe your integration to an external service webhook. +You will also have to provide a webhookUrl configuration to send outgoing messages to. + +> Explain how to configure your integration and list prerequisites `ex: accounts, etc.`. +> You might also want to add configuration details for specific use cases. + +## Usage + +Send messages through the `webhook` channel to send them through the external webhookUrl you configured. +Receive messages through the integration's incoming webhook handler. Received messages have to contain: + +```typescript +userId: string +conversationId: string +text: string +``` + +> Explain how to use your integration. +> You might also want to include an example if there is a specific use case. + +## Limitations + +Only text messages are supported for outgoing messages. + +> List the known bugs. +> List known limits `ex: rate-limiting, payload sizes, etc.` +> List unsupported use cases. + +## Changelog + +- 0.1.0: Incoming webhook and outgoing channel. + +> If some versions of your integration introduce changes worth mentionning (breaking changes, bug fixes), describe them here. This will help users to know what to expect when updating the integration. + +### Integration publication checklist + +- [ ] The register handler is implemented and validates the configuration. +- [ ] Title and descriptions for all schemas are present in `integration.definition.ts`. +- [ ] Events store `conversationId`, `userId` and `messageId` when available. +- [ ] Implement events & actions that are related to `channels`, `entities`, `user`, `conversations` and `messages`. +- [ ] Events related to messages are implemented as messages. +- [ ] When an action is required by the bot developer, a `RuntimeError` is thrown with instructions to fix the problem. +- [ ] Bot name and bot avatar URL fields are available in the integration configuration. diff --git a/packages/cli/templates/webhook-message/package.json b/packages/cli/templates/webhook-message/package.json index 30788c08add..1eb3895e10e 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.24.2", - "@botpress/sdk": "4.15.7", + "@botpress/sdk": "4.15.8", "axios": "^1.6.8" }, "devDependencies": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 923a7f1343f..6492a23fb15 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/sdk", - "version": "4.15.7", + "version": "4.15.8", "description": "Botpress SDK", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/sdk/src/bot/server/index.ts b/packages/sdk/src/bot/server/index.ts index 680bb5162b4..32e769c5702 100644 --- a/packages/sdk/src/bot/server/index.ts +++ b/packages/sdk/src/bot/server/index.ts @@ -220,7 +220,7 @@ const onEventReceived = async (serverProps: types.ServerProps): Promise[string], } - const stateHandlers = self.stateExpiredHandlers['*'] ?? [] + const stateHandlers = self.stateExpiredHandlers[state.name] ?? [] for (const handler of stateHandlers) { await handler(statePayload) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5cb8f4b3ab..127576b19c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -680,6 +680,25 @@ importers: specifier: workspace:* version: link:../../packages/common + integrations/docusign: + dependencies: + '@botpress/client': + specifier: workspace:* + version: link:../../packages/client + '@botpress/sdk': + specifier: workspace:* + version: link:../../packages/sdk + axios: + specifier: ^1.12.2 + version: 1.12.2 + docusign-esign: + specifier: ^8.4.0 + version: 8.4.0 + devDependencies: + '@types/docusign-esign': + specifier: ^5.19.9 + version: 5.19.9 + integrations/dropbox: dependencies: '@botpress/common': @@ -2141,7 +2160,7 @@ importers: specifier: 1.24.2 version: link:../client '@botpress/sdk': - specifier: 4.15.7 + specifier: 4.15.8 version: link:../sdk '@bpinternal/const': specifier: ^0.1.0 @@ -2256,7 +2275,7 @@ importers: specifier: 1.24.2 version: link:../../../client '@botpress/sdk': - specifier: 4.15.7 + specifier: 4.15.8 version: link:../../../sdk devDependencies: '@types/node': @@ -2272,7 +2291,7 @@ importers: specifier: 1.24.2 version: link:../../../client '@botpress/sdk': - specifier: 4.15.7 + specifier: 4.15.8 version: link:../../../sdk devDependencies: '@types/node': @@ -2285,7 +2304,7 @@ importers: packages/cli/templates/empty-plugin: dependencies: '@botpress/sdk': - specifier: 4.15.7 + specifier: 4.15.8 version: link:../../../sdk devDependencies: '@types/node': @@ -2301,7 +2320,7 @@ importers: specifier: 1.24.2 version: link:../../../client '@botpress/sdk': - specifier: 4.15.7 + specifier: 4.15.8 version: link:../../../sdk devDependencies: '@types/node': @@ -2317,7 +2336,7 @@ importers: specifier: 1.24.2 version: link:../../../client '@botpress/sdk': - specifier: 4.15.7 + specifier: 4.15.8 version: link:../../../sdk axios: specifier: ^1.6.8 @@ -2489,7 +2508,7 @@ importers: version: 3.4.2 ulid: specifier: ^2.3.0 - version: 2.3.0 + version: 2.4.0 dependenciesMeta: '@bpinternal/zui': injected: true @@ -3639,6 +3658,9 @@ packages: '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@devhigley/parse-proxy@1.0.3': + resolution: {integrity: sha512-ozRQ9CgWF4JXNNae1zUEpb2fbqH61oxtZz2sdR7a0ci5mi9pSP3EvoU7g4idZYi+CXP32gsvH7kTYZJCGW3DKQ==} + '@doist/todoist-api-typescript@3.0.3': resolution: {integrity: sha512-rDYE6X/xSF+b+fvRYoVyBa7EaUOSIKhTrn7/XxV3Fm2rQrK61SoImor5pyWZlMiABH44WMf+FBIh+IjGAGaX3Q==} peerDependencies: @@ -5549,6 +5571,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/docusign-esign@5.19.9': + resolution: {integrity: sha512-AZNfmxucaY5IGU0j3LH/3A9iFbdY39+wlUnmGfeN0AihCspNC1H08xlQ8j8th5Rl2y9TsgDXcrmKF1ncYVGYWw==} + '@types/es-aggregate-error@1.0.6': resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==} @@ -6147,6 +6172,9 @@ packages: axios@1.11.0: resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + axios@1.2.5: resolution: {integrity: sha512-9pU/8mmjSSOb4CXVsvGIevN+MlO/t9OWtKadTaLuN85Gge3HGorUckgp8A/2FH4V4hJ7JuQ3LIeI7KAV9ITZrQ==} @@ -6652,8 +6680,12 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - d@1.0.1: - resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} + csv-stringify@1.1.2: + resolution: {integrity: sha512-3NmNhhd+AkYs5YtM1GEh01VR6PKj6qch2ayfQaltx5xpcAdThjnbbI5eT8CzRVpXfGKAxnmrSYLsNl/4f3eWiw==} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} dashdash@1.14.1: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} @@ -6873,6 +6905,10 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + docusign-esign@8.4.0: + resolution: {integrity: sha512-vLeDSDfCRUtQG7u50HSKarz8bmBqAfDPJXgi5hjdwB/43tYtTEt/lWiHivxYOKNqxwWrAoBGfAXGlflkEnUOrg==} + engines: {node: '>=2.2.1'} + dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} @@ -7038,6 +7074,10 @@ packages: resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==} engines: {node: '>=0.10'} + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + es6-iterator@2.0.3: resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} @@ -7183,6 +7223,10 @@ packages: jiti: optional: true + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8539,6 +8583,10 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -9072,6 +9120,9 @@ packages: oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + oauth@0.10.2: + resolution: {integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -9256,6 +9307,14 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + passport-oauth2@1.8.0: + resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==} + engines: {node: '>= 0.4.0'} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -10470,9 +10529,6 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - type@1.2.0: - resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} - type@2.7.2: resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} @@ -10522,8 +10578,11 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - ulid@2.3.0: - resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} + uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + + ulid@2.4.0: + resolution: {integrity: sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==} hasBin: true unbox-primitive@1.1.0: @@ -12553,6 +12612,8 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 + '@devhigley/parse-proxy@1.0.3': {} + '@doist/todoist-api-typescript@3.0.3(type-fest@4.41.0)': dependencies: axios: 1.11.0 @@ -14680,6 +14741,10 @@ snapshots: dependencies: '@types/ms': 0.7.31 + '@types/docusign-esign@5.19.9': + dependencies: + '@types/node': 22.16.4 + '@types/es-aggregate-error@1.0.6': dependencies: '@types/node': 22.16.4 @@ -15284,7 +15349,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 arrify@2.0.1: {} @@ -15394,6 +15459,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.12.2: + dependencies: + follow-redirects: 1.15.6(debug@4.4.1) + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.2.5: dependencies: follow-redirects: 1.15.6(debug@4.4.1) @@ -15930,7 +16003,7 @@ snapshots: cli-color@2.0.3: dependencies: - d: 1.0.1 + d: 1.0.2 es5-ext: 0.10.62 es6-iterator: 2.0.3 memoizee: 0.4.15 @@ -16065,10 +16138,14 @@ snapshots: csstype@3.1.3: {} - d@1.0.1: + csv-stringify@1.1.2: dependencies: - es5-ext: 0.10.62 - type: 1.2.0 + lodash.get: 4.4.2 + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.2 dashdash@1.14.1: dependencies: @@ -16248,6 +16325,17 @@ snapshots: dependencies: esutils: 2.0.3 + docusign-esign@8.4.0: + dependencies: + '@devhigley/parse-proxy': 1.0.3 + axios: 1.12.2 + csv-stringify: 1.1.2 + jsonwebtoken: 9.0.2 + passport-oauth2: 1.8.0 + safe-buffer: 5.2.1 + transitivePeerDependencies: + - debug + dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 @@ -16499,7 +16587,7 @@ snapshots: es-set-tostringtag@2.1.0: dependencies: es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -16523,9 +16611,16 @@ snapshots: es6-symbol: 3.1.3 next-tick: 1.1.0 + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.3 + esniff: 2.0.1 + next-tick: 1.1.0 + es6-iterator@2.0.3: dependencies: - d: 1.0.1 + d: 1.0.2 es5-ext: 0.10.62 es6-symbol: 3.1.3 @@ -16533,12 +16628,12 @@ snapshots: es6-symbol@3.1.3: dependencies: - d: 1.0.1 + d: 1.0.2 ext: 1.7.0 es6-weak-map@2.0.3: dependencies: - d: 1.0.1 + d: 1.0.2 es5-ext: 0.10.62 es6-iterator: 2.0.3 es6-symbol: 3.1.3 @@ -16796,6 +16891,13 @@ snapshots: transitivePeerDependencies: - supports-color + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.2 + espree@10.4.0: dependencies: acorn: 8.15.0 @@ -16824,7 +16926,7 @@ snapshots: event-emitter@0.3.5: dependencies: - d: 1.0.1 + d: 1.0.2 es5-ext: 0.10.62 event-source-polyfill@1.0.31: {} @@ -17261,7 +17363,7 @@ snapshots: dependencies: call-bound: 1.0.3 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-tsconfig@4.10.1: dependencies: @@ -17683,7 +17785,7 @@ snapshots: dependencies: call-bind: 1.0.8 call-bound: 1.0.3 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-arrayish@0.2.1: {} @@ -17722,7 +17824,7 @@ snapshots: is-data-view@1.0.2: dependencies: call-bound: 1.0.3 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-typed-array: 1.1.15 is-date-object@1.0.5: @@ -17845,7 +17947,7 @@ snapshots: is-weakset@2.0.4: dependencies: call-bound: 1.0.3 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-wsl@2.2.0: dependencies: @@ -18493,6 +18595,8 @@ snapshots: lodash.defaults@4.2.0: {} + lodash.get@4.4.2: {} + lodash.includes@4.3.0: {} lodash.isarguments@3.1.0: {} @@ -18749,7 +18853,7 @@ snapshots: memoizee@0.4.15: dependencies: - d: 1.0.1 + d: 1.0.2 es5-ext: 0.10.62 es6-weak-map: 2.0.3 event-emitter: 0.3.5 @@ -19172,6 +19276,8 @@ snapshots: oauth-sign@0.9.0: {} + oauth@0.10.2: {} + object-assign@4.1.1: {} object-inspect@1.13.3: {} @@ -19292,7 +19398,7 @@ snapshots: own-keys@1.0.1: dependencies: - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 @@ -19383,6 +19489,16 @@ snapshots: no-case: 3.0.4 tslib: 2.6.2 + passport-oauth2@1.8.0: + dependencies: + base64url: 3.0.1 + oauth: 0.10.2 + passport-strategy: 1.0.0 + uid2: 0.0.4 + utils-merge: 1.0.1 + + passport-strategy@1.0.0: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -19704,7 +19820,7 @@ snapshots: es-abstract: 1.23.9 es-errors: 1.3.0 es-object-atoms: 1.0.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -19884,7 +20000,7 @@ snapshots: dependencies: call-bind: 1.0.8 call-bound: 1.0.3 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 @@ -19988,7 +20104,7 @@ snapshots: define-data-property: 1.1.4 es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 gopd: 1.2.0 has-property-descriptors: 1.0.2 @@ -20049,14 +20165,14 @@ snapshots: dependencies: call-bound: 1.0.3 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-inspect: 1.13.3 side-channel-weakmap@1.0.2: dependencies: call-bound: 1.0.3 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-inspect: 1.13.3 side-channel-map: 1.0.1 @@ -20719,8 +20835,6 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - type@1.2.0: {} - type@2.7.2: {} typed-array-buffer@1.0.3: @@ -20778,7 +20892,9 @@ snapshots: uglify-js@3.19.3: optional: true - ulid@2.3.0: {} + uid2@0.0.4: {} + + ulid@2.4.0: {} unbox-primitive@1.1.0: dependencies: