diff --git a/integrations/hubspot/definitions/actions/company.ts b/integrations/hubspot/definitions/actions/company.ts index 5f7776dfb7f..5f2b52891aa 100644 --- a/integrations/hubspot/definitions/actions/company.ts +++ b/integrations/hubspot/definitions/actions/company.ts @@ -11,7 +11,7 @@ export const companySchema = z.object({ const searchCompany: ActionDefinition = { title: 'Search Company', - description: 'Search for a company in Hubspot', + description: 'Search for a company in HubSpot', input: { schema: z.object({ name: z.string().optional().title('Name').describe('The name of the company to search for'), @@ -27,7 +27,7 @@ const searchCompany: ActionDefinition = { const getCompany: ActionDefinition = { title: 'Get Company', - description: 'Get a company from Hubspot by ID', + description: 'Get a company from HubSpot by ID', input: { schema: z.object({ companyId: z.string().title('Company ID').describe('The ID of the company to get'), @@ -49,7 +49,7 @@ const getCompany: ActionDefinition = { const updateCompany: ActionDefinition = { title: 'Update Company', - description: 'Update a company in Hubspot', + description: 'Update a company in HubSpot', input: { schema: z.object({ companyId: z.string().title('Company ID').describe('The ID of the company to update'), diff --git a/integrations/hubspot/definitions/actions/deal.ts b/integrations/hubspot/definitions/actions/deal.ts index e993a83cc08..69dcd5dfa2d 100644 --- a/integrations/hubspot/definitions/actions/deal.ts +++ b/integrations/hubspot/definitions/actions/deal.ts @@ -10,7 +10,7 @@ export const dealSchema = z.object({ const searchDeal: ActionDefinition = { title: 'Search Deal', - description: 'Search for a deal in Hubspot', + description: 'Search for a deal in HubSpot', input: { schema: z.object({ name: z.string().optional().title('Name').describe('The name of the deal to search for'), @@ -25,7 +25,7 @@ const searchDeal: ActionDefinition = { const createDeal: ActionDefinition = { title: 'Create Deal', - description: 'Create a deal in Hubspot', + description: 'Create a deal in HubSpot', input: { schema: z.object({ name: z.string().title('Name').describe('The name of the deal'), @@ -50,7 +50,7 @@ const createDeal: ActionDefinition = { const getDeal: ActionDefinition = { title: 'Get Deal', - description: 'Get a deal from Hubspot', + description: 'Get a deal from HubSpot', input: { schema: z.object({ dealId: z.string().title('Deal ID').describe('The ID of the deal to get'), @@ -65,7 +65,7 @@ const getDeal: ActionDefinition = { const updateDeal: ActionDefinition = { title: 'Update Deal', - description: 'Update a deal in Hubspot', + description: 'Update a deal in HubSpot', input: { schema: z.object({ dealId: z.string().title('Deal ID').describe('The ID of the deal to update'), @@ -97,7 +97,7 @@ const updateDeal: ActionDefinition = { const deleteDeal: ActionDefinition = { title: 'Delete Deal', - description: 'Delete a deal in Hubspot', + description: 'Delete a deal in HubSpot', input: { schema: z.object({ dealId: z.string().title('Deal ID').describe('The ID of the deal to delete'), diff --git a/integrations/hubspot/definitions/actions/lead.ts b/integrations/hubspot/definitions/actions/lead.ts index bb5f00acd07..bcf53a1a415 100644 --- a/integrations/hubspot/definitions/actions/lead.ts +++ b/integrations/hubspot/definitions/actions/lead.ts @@ -10,7 +10,7 @@ export const leadSchema = z.object({ const searchLead: ActionDefinition = { title: 'Search Lead', - description: 'Search for a lead in Hubspot', + description: 'Search for a lead in HubSpot', input: { schema: z.object({ name: z.string().optional().title('Name').describe('The name of the lead to search for'), @@ -24,7 +24,7 @@ const searchLead: ActionDefinition = { } const createLead: ActionDefinition = { title: 'Create Lead', - description: 'Create a lead in Hubspot', + description: 'Create a lead in HubSpot', input: { schema: z.object({ name: z.string().title('Name').describe('The name of the lead'), @@ -53,7 +53,7 @@ const createLead: ActionDefinition = { const getLead: ActionDefinition = { title: 'Get Lead', - description: 'Get a lead from Hubspot', + description: 'Get a lead from HubSpot', input: { schema: z.object({ leadId: z.string().title('Lead ID').describe('The ID of the lead to get'), @@ -68,7 +68,7 @@ const getLead: ActionDefinition = { const updateLead: ActionDefinition = { title: 'Update Lead', - description: 'Update a lead in Hubspot', + description: 'Update a lead in HubSpot', input: { schema: z.object({ leadId: z.string().title('Lead ID').describe('The ID of the lead to update'), @@ -100,7 +100,7 @@ const updateLead: ActionDefinition = { const deleteLead: ActionDefinition = { title: 'Delete Lead', - description: 'Delete a lead in Hubspot', + description: 'Delete a lead in HubSpot', input: { schema: z.object({ leadId: z.string().title('Lead ID').describe('The ID of the lead to delete'), diff --git a/integrations/hubspot/definitions/events.ts b/integrations/hubspot/definitions/events.ts index 7425b39342c..1e2085a272f 100644 --- a/integrations/hubspot/definitions/events.ts +++ b/integrations/hubspot/definitions/events.ts @@ -2,7 +2,7 @@ import { z, EventDefinition } from '@botpress/sdk' const contactCreated = { title: 'Contact Created', - description: 'A new contact has been created in Hubspot.', + description: 'A new contact has been created in HubSpot.', schema: z.object({ contactId: z.string().title('Contact ID').describe('The ID of the created contact'), name: z.string().optional().title('Name').describe('The name of the created contact'), @@ -13,7 +13,7 @@ const contactCreated = { const contactDeleted = { title: 'Contact Deleted', - description: 'A contact has been deleted in Hubspot.', + description: 'A contact has been deleted in HubSpot.', schema: z.object({ contactId: z.string().title('Contact ID').describe('The ID of the deleted contact'), }), @@ -21,7 +21,7 @@ const contactDeleted = { const companyCreated = { title: 'Company Created', - description: 'A new company has been created in Hubspot.', + description: 'A new company has been created in HubSpot.', schema: z.object({ companyId: z.string().title('Company ID').describe('The ID of the created company'), name: z.string().optional().title('Name').describe('The name of the created company'), @@ -32,7 +32,7 @@ const companyCreated = { const companyDeleted = { title: 'Company Deleted', - description: 'A company has been deleted in Hubspot.', + description: 'A company has been deleted in HubSpot.', schema: z.object({ companyId: z.string().title('Company ID').describe('The ID of the deleted company'), }), @@ -40,7 +40,7 @@ const companyDeleted = { const ticketCreated = { title: 'Ticket Created', - description: 'A new ticket has been created in Hubspot.', + description: 'A new ticket has been created in HubSpot.', schema: z.object({ ticketId: z.string().title('Ticket ID').describe('The ID of the created ticket'), subject: z.string().optional().title('Subject').describe('The subject of the created ticket'), @@ -53,7 +53,7 @@ const ticketCreated = { const ticketDeleted = { title: 'Ticket Deleted', - description: 'A ticket has been deleted in Hubspot.', + description: 'A ticket has been deleted in HubSpot.', schema: z.object({ ticketId: z.string().title('Ticket ID').describe('The ID of the deleted ticket'), }), diff --git a/integrations/hubspot/definitions/states.ts b/integrations/hubspot/definitions/states.ts index ca276b6aeea..a9269f71434 100644 --- a/integrations/hubspot/definitions/states.ts +++ b/integrations/hubspot/definitions/states.ts @@ -3,8 +3,8 @@ import { z, StateDefinition } from '@botpress/sdk' const oauthCredentials = { type: 'integration', schema: z.object({ - accessToken: z.string().title('Access Token').describe('The access token for the Hubspot integration'), - refreshToken: z.string().title('Refresh Token').describe('The refresh token for the Hubspot integration'), + accessToken: z.string().title('Access Token').describe('The access token for the HubSpot integration'), + refreshToken: z.string().title('Refresh Token').describe('The refresh token for the HubSpot integration'), expiresAtSeconds: z.number().title('Expires At').describe('The timestamp in seconds when the access token expires'), }), } satisfies StateDefinition @@ -69,7 +69,7 @@ const propertyCacheStateDefinition = { z.object({ label: z.string().title('Label').describe('The label of the property'), type: propertyTypeSchema, - hubspotDefined: z.boolean().title('Hubspot Defined').describe('Whether the property is defined by Hubspot'), + hubspotDefined: z.boolean().title('HubSpot Defined').describe('Whether the property is defined by HubSpot'), options: z .array(z.string()) .optional() diff --git a/integrations/hubspot/hub.md b/integrations/hubspot/hub.md index 0baf5943931..c9bf6a74048 100644 --- a/integrations/hubspot/hub.md +++ b/integrations/hubspot/hub.md @@ -67,7 +67,7 @@ Under the **Webhooks** tab, subscribe to: #### 4. Get Your App ID and Developer API Key - **App ID**: Open your private App in HubSpot again — the App ID is in the URL (e.g., `https://app.hubspot.com/private-apps/ACCOUNT_ID/36900466`). -- **Developer API Key**: In your Hubspot Dashboard, navigate to _Development_ > _Keys_ > _Developer API Key_ and copy or generate your key. +- **Developer API Key**: In your HubSpot Dashboard, navigate to _Development_ > _Keys_ > _Developer API Key_ and copy or generate your key. #### 5. Retrieve Your Help Desk or Inbox IDs diff --git a/integrations/hubspot/integration.definition.ts b/integrations/hubspot/integration.definition.ts index a4b29017598..c3a15d6a19a 100644 --- a/integrations/hubspot/integration.definition.ts +++ b/integrations/hubspot/integration.definition.ts @@ -6,7 +6,7 @@ export default new IntegrationDefinition({ name: 'hubspot', title: 'HubSpot', description: 'Manage contacts, tickets and more from your chatbot.', - version: '6.0.1', + version: '6.0.4', readme: 'hub.md', icon: 'icon.svg', configuration: { @@ -18,7 +18,7 @@ export default new IntegrationDefinition({ configurations: { manual: { title: 'Manual Configuration', - description: 'Manual configuration, use your own Hubspot app', + description: 'Manual configuration, use your own HubSpot app', schema: z.object({ accessToken: z .string() @@ -72,10 +72,10 @@ export default new IntegrationDefinition({ }, secrets: { CLIENT_ID: { - description: 'The client ID of the Hubspot app', + description: 'The client ID of the HubSpot app', }, CLIENT_SECRET: { - description: 'The client secret of the Hubspot app', + description: 'The client secret of the HubSpot app', }, DISABLE_OAUTH: { // TODO: Remove once the OAuth app allows for unlimited installs diff --git a/integrations/hubspot/linkTemplate.vrl b/integrations/hubspot/linkTemplate.vrl index 763759a0bbb..5995e59a41b 100644 --- a/integrations/hubspot/linkTemplate.vrl +++ b/integrations/hubspot/linkTemplate.vrl @@ -1,6 +1,6 @@ webhookId = to_string!(.webhookId) webhookUrl = to_string!(.webhookUrl) -source = to_string(.source) ?? "" +source = to_string!(.source) if source == "desk" { "{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}&wizchoice=without-hitl" diff --git a/integrations/hubspot/src/webhook/handler.ts b/integrations/hubspot/src/webhook/handler.ts index b82c83360f1..78b5a33fa24 100644 --- a/integrations/hubspot/src/webhook/handler.ts +++ b/integrations/hubspot/src/webhook/handler.ts @@ -1,5 +1,6 @@ import { generateRedirection } from '@botpress/common/src/html-dialogs' import * as oauthWizard from '@botpress/common/src/oauth-wizard' +import { OAUTH_IDENTIFIER_HEADER } from '@botpress/sdk' import { Signature } from '@hubspot/api-client' import { getClientSecret } from '../auth' import { handleOperatorReplied } from '../hitl/events/operator-replied' @@ -25,7 +26,14 @@ export const handler: bp.IntegrationProps['handler'] = async (props) => { if (req.path.startsWith('/oauth')) { const modifiedProps = { ...props, req: { ...props.req, path: '/oauth/wizard/oauth-callback' } } try { - return await buildOAuthWizard(modifiedProps).handleRequest() + const wizardResult = await buildOAuthWizard(modifiedProps).handleRequest() + const identifier = wizardResult.headers?.[OAUTH_IDENTIFIER_HEADER] + return identifier + ? { + status: 200, + headers: { [OAUTH_IDENTIFIER_HEADER]: identifier }, + } + : wizardResult } catch (thrown: unknown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) return generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) diff --git a/integrations/linear/definitions/states.ts b/integrations/linear/definitions/states.ts index e6d3e86fed2..2bf6faf2eb2 100644 --- a/integrations/linear/definitions/states.ts +++ b/integrations/linear/definitions/states.ts @@ -13,6 +13,16 @@ export const states = { expiresAt: z.string().title('Expires At').describe('The time when the access token expires'), }), }, + environment: { + type: 'integration', + schema: z.object({ + env: z + .enum(['preview', 'production']) + .title('Environment') + .describe('The environment where the integration is installed'), + source: z.string().optional().title('Source').describe('The source of the OAuth request, eg: "desk"'), + }), + }, // TODO: delete these 2 states when the backend stop considering state deletion as breaking change configuration: { diff --git a/integrations/linear/integration.definition.ts b/integrations/linear/integration.definition.ts index ee0d5e162b2..4fe14e6dafb 100644 --- a/integrations/linear/integration.definition.ts +++ b/integrations/linear/integration.definition.ts @@ -9,7 +9,7 @@ import listable from './bp_modules/listable' import { actions, channels, events, configuration, configurations, user, states, entities } from './definitions' export const INTEGRATION_NAME = 'linear' -export const INTEGRATION_VERSION = '2.4.0' +export const INTEGRATION_VERSION = '2.5.0' export default new IntegrationDefinition({ name: INTEGRATION_NAME, @@ -37,6 +37,12 @@ export default new IntegrationDefinition({ CLIENT_SECRET: { description: 'The client secret of your Linear OAuth app.', }, + DESK_CLIENT_ID: { + description: 'The client ID of your Linear OAuth app.', + }, + DESK_CLIENT_SECRET: { + description: 'The client secret of your Linear OAuth app.', + }, WEBHOOK_SIGNING_SECRET: { description: 'The signing secret of your Linear webhook.', }, diff --git a/integrations/linear/linkTemplate.vrl b/integrations/linear/linkTemplate.vrl index eaf66939cad..90e884c1946 100644 --- a/integrations/linear/linkTemplate.vrl +++ b/integrations/linear/linkTemplate.vrl @@ -1,11 +1,5 @@ webhookId = to_string!(.webhookId) webhookUrl = to_string!(.webhookUrl) -env = to_string!(.env) +source = to_string!(.source) -clientId = "364cc18b5fd2edc8abc3b7113e6a7908" - -if env == "production" { - clientId = "be8aaf51ad3d057ed5870c5729926929" -} - -"https://linear.app/oauth/authorize?client_id={{ clientId }}&redirect_uri={{ webhookUrl }}/oauth&response_type=code&prompt=consent&actor=application&state={{ webhookId }}&scope=read,write,issues:create,comments:create,admin" +"{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}&source={{ source }}" diff --git a/integrations/linear/src/handler.ts b/integrations/linear/src/handler.ts index 32ed6b7063d..222f1167be8 100644 --- a/integrations/linear/src/handler.ts +++ b/integrations/linear/src/handler.ts @@ -1,13 +1,16 @@ -import { Request, RuntimeError } from '@botpress/sdk' +import { generateRedirection } from '@botpress/common/src/html-dialogs' +import * as oauthWizard from '@botpress/common/src/oauth-wizard' +import { Request, RuntimeError, OAUTH_IDENTIFIER_HEADER } from '@botpress/sdk' import { LinearWebhookClient } from '@linear/sdk/webhooks' import { fireIssueCreated } from './events/issueCreated' import { fireIssueDeleted } from './events/issueDeleted' import { fireIssueUpdated } from './events/issueUpdated' import * as mapping from './files-readonly/mapping' -import { LinearEvent, LinearIssueEvent, handleOauth } from './misc/linear' +import { LinearEvent, LinearIssueEvent } from './misc/linear' import { Result } from './misc/types' import { getLinearClient, getUserAndConversation } from './misc/utils' +import { buildOAuthWizard } from './oauth-wizard' import * as bp from '.botpress' const LINEAR_WEBHOOK_SIGNATURE_HEADER = 'linear-signature' @@ -21,12 +24,33 @@ export const handler: bp.IntegrationProps['handler'] = async (props) => { `Linear handler invoked (method="${req.method ?? ''}", path="${req.path ?? ''}", hasBody=${Boolean(req.body)})` ) + if (oauthWizard.isOAuthWizardUrl(req.path)) { + try { + return await buildOAuthWizard(props).handleRequest() + } catch (thrown: unknown) { + const errMsg = thrown instanceof Error ? thrown.message : String(thrown) + logger.forBot().error('Error while processing OAuth wizard request', errMsg) + return generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) + } + } + if (req.path === '/oauth') { logger.forBot().info('Linear OAuth callback received') - return await handleOauth(props).catch((err) => { - logger.forBot().error('Error while processing OAuth', err.response?.data || err.message) - throw err - }) + const modifiedProps = { ...props, req: { ...props.req, path: '/oauth/wizard/oauth-callback' } } + try { + const wizardResult = await buildOAuthWizard(modifiedProps).handleRequest() + const identifier = wizardResult.headers?.[OAUTH_IDENTIFIER_HEADER] + return identifier + ? { + status: 200, + headers: { [OAUTH_IDENTIFIER_HEADER]: identifier }, + } + : wizardResult + } catch (thrown: unknown) { + const errMsg = thrown instanceof Error ? thrown.message : String(thrown) + logger.forBot().error('Error while processing OAuth callback', errMsg) + return generateRedirection(oauthWizard.getInterstitialUrl(false, errMsg)) + } } if (!req.body) { diff --git a/integrations/linear/src/misc/linear.ts b/integrations/linear/src/misc/linear.ts index 09a7d8464a9..31a779137d5 100644 --- a/integrations/linear/src/misc/linear.ts +++ b/integrations/linear/src/misc/linear.ts @@ -1,7 +1,7 @@ -import { OAUTH_IDENTIFIER_HEADER, Response, RuntimeError, z } from '@botpress/sdk' +import { RuntimeError, z } from '@botpress/sdk' import { LinearClient } from '@linear/sdk' import axios from 'axios' -import queryString from 'query-string' +import { useDeskOAuth } from './utils' import * as bp from '.botpress' type Credentials = bp.states.States['credentials']['payload'] @@ -107,9 +107,9 @@ export class LinearOauthClient { private _clientSecret: string private _redirectUri: string - public constructor() { - this._clientId = bp.secrets.CLIENT_ID - this._clientSecret = bp.secrets.CLIENT_SECRET + public constructor(useDeskOAuth?: boolean) { + this._clientId = useDeskOAuth ? bp.secrets.DESK_CLIENT_ID : bp.secrets.CLIENT_ID + this._clientSecret = useDeskOAuth ? bp.secrets.DESK_CLIENT_SECRET : bp.secrets.CLIENT_SECRET this._redirectUri = `${process.env.BP_WEBHOOK_URL}/oauth` } @@ -117,12 +117,21 @@ export class LinearOauthClient { url: string, body: z.infer ): Promise { - const { data } = await axios.post( - url, - { client_id: this._clientId, client_secret: this._clientSecret, ...body }, - { headers: oauthHeaders } - ) - return data + const form = new URLSearchParams({ + client_id: this._clientId, + client_secret: this._clientSecret, + ...body, + }) + try { + const response = await axios.post(url, form.toString(), { headers: oauthHeaders }) + return response.data + } catch (err) { + if (axios.isAxiosError(err)) { + const message = err.response?.data?.error_description || err.message + throw new RuntimeError(`OAuth request failed: ${message}`) + } + throw new RuntimeError(`OAuth request failed: ${String(err)}`) + } } private _parseCredentials(res: OAuthResponse): Credentials { @@ -198,7 +207,15 @@ export class LinearOauthClient { id: ctx.integrationId, }) - const linearOauthClient = new LinearOauthClient() + const { + state: { payload: environment }, + } = await client.getState({ + type: 'integration', + name: 'environment', + id: ctx.integrationId, + }) + const useDesk = useDeskOAuth(environment) + const linearOauthClient = new LinearOauthClient(useDesk) const credentials = await linearOauthClient.resolveValidCredentials(payload) if (credentials.accessToken !== payload.accessToken) { @@ -268,40 +285,3 @@ export const registerWebhook = async ({ }) logger.forBot().info('Linear webhook registered successfully.') } - -export const handleOauth = async ({ req, ctx, client, logger }: bp.HandlerProps): Promise => { - const linearOauthClient = new LinearOauthClient() - - const query = queryString.parse(req.query) - const code = query.code - - if (typeof code !== 'string') { - throw new RuntimeError('Handler received an empty code') - } - - const credentials = await linearOauthClient.getAccessTokenFromOAuthCode(code) - logger.forBot().info('Obtained credentials from OAuth flow, saving to state...') - await client.setState({ - type: 'integration', - name: 'credentials', - id: ctx.integrationId, - payload: credentials, - }) - - const linearClient = new LinearClient({ accessToken: credentials.accessToken }) - const organization = await linearClient.organization - await client.configureIntegration({ scheduleRegisterCall: 'monthly' }) - - const webhookUrl = `${process.env.BP_WEBHOOK_URL}/${ctx.webhookId}` - try { - await registerWebhook({ linearClient, logger, url: webhookUrl }) - } catch (thrown) { - const errorMessage = thrown instanceof Error ? thrown.message : String(thrown) - logger.forBot().warn('Failed to register webhook:', errorMessage) - } - - return { - status: 200, - headers: { [OAUTH_IDENTIFIER_HEADER]: organization.id }, - } -} diff --git a/integrations/linear/src/misc/utils.ts b/integrations/linear/src/misc/utils.ts index 2cff106c254..9e8659e7415 100644 --- a/integrations/linear/src/misc/utils.ts +++ b/integrations/linear/src/misc/utils.ts @@ -11,6 +11,9 @@ export async function getLinearClient({ client, ctx }: LinearClientProps) { return await LinearOauthClient.create({ client, ctx }) } +export const useDeskOAuth = ({ env, source }: bp.states.environment.Environment['payload']) => + env === 'production' && source === 'desk' + type ValueOf = T[keyof T] type CreateCommentProps = Omit, 'payload'> & { content: string } export async function createComment(args: CreateCommentProps) { diff --git a/integrations/linear/src/oauth-wizard.ts b/integrations/linear/src/oauth-wizard.ts new file mode 100644 index 00000000000..2f014cc9aa8 --- /dev/null +++ b/integrations/linear/src/oauth-wizard.ts @@ -0,0 +1,97 @@ +import * as oauthWizard from '@botpress/common/src/oauth-wizard' +import { z } from '@botpress/sdk' +import { LinearClient } from '@linear/sdk' +import { LinearOauthClient, registerWebhook } from './misc/linear' +import { useDeskOAuth } from './misc/utils' +import * as bp from '.botpress' + +const REDIRECT_URI = `${process.env.BP_WEBHOOK_URL}/oauth` +const SCOPES = 'read,write,issues:create,comments:create,admin' + +const _startStep: oauthWizard.WizardStepHandler = async ({ ctx, client, query, responses }) => { + const searchParams = new URLSearchParams(query) + const payload = { + source: searchParams.get('source') ?? undefined, + env: z.enum(['preview', 'production']).catch('preview').parse(searchParams.get('env')), + } as bp.states.environment.Environment['payload'] + + await client.setState({ + type: 'integration', + name: 'environment', + id: ctx.integrationId, + payload, + }) + + const isDesk = useDeskOAuth(payload) + const clientId = isDesk ? bp.secrets.DESK_CLIENT_ID : bp.secrets.CLIENT_ID + const actor = isDesk ? 'user' : 'application' + const authorizeUrl = + 'https://linear.app/oauth/authorize' + + `?client_id=${clientId}` + + `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` + + '&response_type=code' + + '&prompt=consent' + + `&actor=${actor}` + + `&state=${ctx.webhookId}` + + `&scope=${SCOPES}` + + return responses.redirectToExternalUrl(authorizeUrl) +} + +const _oauthCallbackStep: oauthWizard.WizardStepHandler = async ({ + ctx, + client, + logger, + query, + responses, + setIntegrationIdentifier, +}) => { + const error = query.get('error') + if (error) { + const description = query.get('error_description') ?? '' + return responses.endWizard({ success: false, errorMessage: `OAuth error: ${error} - ${description}` }) + } + + const code = query.get('code') + if (!code) { + return responses.endWizard({ success: false, errorMessage: 'Authorization code not present in OAuth callback' }) + } + + const { + state: { payload: environment }, + } = await client.getState({ + type: 'integration', + name: 'environment', + id: ctx.integrationId, + }) + const useDesk = useDeskOAuth(environment) + const linearOauthClient = new LinearOauthClient(useDesk) + const credentials = await linearOauthClient.getAccessTokenFromOAuthCode(code) + logger.forBot().info('Obtained credentials from OAuth flow, saving to state...') + await client.setState({ + type: 'integration', + name: 'credentials', + id: ctx.integrationId, + payload: credentials, + }) + + const linearClient = new LinearClient({ accessToken: credentials.accessToken }) + const organization = await linearClient.organization + + const webhookUrl = `${process.env.BP_WEBHOOK_URL}/${ctx.webhookId}` + try { + await registerWebhook({ linearClient, logger, url: webhookUrl }) + } catch (thrown) { + const errorMessage = thrown instanceof Error ? thrown.message : String(thrown) + logger.forBot().warn('Failed to register webhook:', errorMessage) + } + + setIntegrationIdentifier(organization.id) + return responses.endWizard({ success: true }) +} + +export const buildOAuthWizard = (props: bp.HandlerProps) => + new oauthWizard.OAuthWizardBuilder(props) + .addStep({ id: 'start', handler: _startStep }) + .addStep({ id: 'oauth-callback', handler: _oauthCallbackStep }) + .build() diff --git a/integrations/linear/tsconfig.json b/integrations/linear/tsconfig.json index d46abc5b88f..b94d8463ee4 100644 --- a/integrations/linear/tsconfig.json +++ b/integrations/linear/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact", "paths": { "*": ["./*"] }, "outDir": "dist" }, diff --git a/packages/llmz/package.json b/packages/llmz/package.json index 3f88d38fd11..bc080d3f6e5 100644 --- a/packages/llmz/package.json +++ b/packages/llmz/package.json @@ -2,7 +2,7 @@ "name": "llmz", "type": "module", "description": "LLMz - An LLM-native Typescript VM built on top of Zui", - "version": "0.0.76", + "version": "0.0.77", "types": "./dist/index.d.ts", "main": "./dist/index.cjs", "module": "./dist/index.js", diff --git a/packages/zai/package.json b/packages/zai/package.json index 833db27e6f7..c1ecaec5e16 100644 --- a/packages/zai/package.json +++ b/packages/zai/package.json @@ -1,7 +1,7 @@ { "name": "@botpress/zai", "description": "Zui AI (zai) – An LLM utility library written on top of Zui and the Botpress API", - "version": "2.6.20", + "version": "2.6.21", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { diff --git a/packages/zai/src/context.ts b/packages/zai/src/context.ts index 777d189e514..2f38e1f2a82 100644 --- a/packages/zai/src/context.ts +++ b/packages/zai/src/context.ts @@ -15,7 +15,7 @@ export type ZaiContextProps = { client: Cognitive taskType: string taskId: string - modelId: GenerateContentInput['model'] + modelId: string | string[] adapter?: Adapter source?: GenerateContentInput['meta'] memoizer?: Memoizer