diff --git a/integrations/dropbox/integration.definition.ts b/integrations/dropbox/integration.definition.ts index a51e6e297f3..3e273f05ec8 100644 --- a/integrations/dropbox/integration.definition.ts +++ b/integrations/dropbox/integration.definition.ts @@ -5,7 +5,7 @@ import { actions, configuration, configurations, entities, secrets, states } fro export default new sdk.IntegrationDefinition({ name: 'dropbox', title: 'Dropbox', - version: '2.0.1', + version: '2.0.2', description: 'Manage your files and folders effortlessly.', readme: 'hub.md', icon: 'icon.svg', diff --git a/integrations/dropbox/src/webhook-events/oauth/wizard.ts b/integrations/dropbox/src/webhook-events/oauth/wizard.ts index c90cd943900..db11ed92e1e 100644 --- a/integrations/dropbox/src/webhook-events/oauth/wizard.ts +++ b/integrations/dropbox/src/webhook-events/oauth/wizard.ts @@ -16,8 +16,15 @@ export const handler = async (props: bp.HandlerProps) => { return response } -const _startHandler: WizardHandler = (props) => { +const _startHandler: WizardHandler = async (props) => { const { responses } = props + + // When nothing is connected yet there's nothing to reset, so skip the + // confirmation and go straight to authorizing with Dropbox. + if (!(await _isAlreadyConnected(props))) { + return _redirectToDropboxHandler(props) + } + return responses.displayButtons({ pageTitle: 'Reset Configuration', htmlOrMarkdownPageContents: @@ -38,6 +45,19 @@ const _startHandler: WizardHandler = (props) => { }) } +const _isAlreadyConnected = async ({ client, ctx }: bp.HandlerProps): Promise => { + try { + const result = await client.getState({ + id: ctx.integrationId, + type: 'integration', + name: 'authorization', + }) + return Boolean(result?.state?.payload?.refreshToken) + } catch { + return false + } +} + const _redirectToDropboxHandler: WizardHandler = async (props) => { const { responses, ctx } = props const clientId = getOAuthClientId({ ctx }) diff --git a/integrations/messenger/integration.definition.ts b/integrations/messenger/integration.definition.ts index c10b635c683..6d22ca4e74f 100644 --- a/integrations/messenger/integration.definition.ts +++ b/integrations/messenger/integration.definition.ts @@ -8,7 +8,7 @@ import { actions } from './definitions/actions' import { messages } from './definitions/channels/channel/messages' export const INTEGRATION_NAME = 'messenger' -export const INTEGRATION_VERSION = '5.1.8' +export const INTEGRATION_VERSION = '5.1.9' const commonConfigSchema = z.object({ downloadMedia: z diff --git a/integrations/messenger/src/webhook/handlers/oauth/wizard.ts b/integrations/messenger/src/webhook/handlers/oauth/wizard.ts index 191539bdbac..040e3e20035 100644 --- a/integrations/messenger/src/webhook/handlers/oauth/wizard.ts +++ b/integrations/messenger/src/webhook/handlers/oauth/wizard.ts @@ -21,7 +21,15 @@ export const handler = async (props: bp.HandlerProps) => { return response } -const _startHandler: WizardHandler = ({ responses }) => { +const _startHandler: WizardHandler = async (props) => { + const { responses } = props + + // When nothing is connected yet there's nothing to reset, so skip the + // confirmation and go straight to the next step. + if (!(await _isAlreadyConnected(props))) { + return _resetHandler(props) + } + return responses.displayButtons({ pageTitle: 'Reset Configuration', htmlOrMarkdownPageContents: ` @@ -43,6 +51,15 @@ const _startHandler: WizardHandler = ({ responses }) => { }) } +const _isAlreadyConnected = async ({ client, ctx }: bp.HandlerProps): Promise => { + try { + const result = await client.getState({ type: 'integration', name: 'oauth', id: ctx.integrationId }) + return Boolean(result?.state?.payload?.pageToken) + } catch { + return false + } +} + const _resetHandler: WizardHandler = async ({ responses, client, ctx }) => { await client.setState({ type: 'integration', diff --git a/integrations/whatsapp/integration.definition.ts b/integrations/whatsapp/integration.definition.ts index 3a690a7a4ce..7c6a017cfd5 100644 --- a/integrations/whatsapp/integration.definition.ts +++ b/integrations/whatsapp/integration.definition.ts @@ -157,7 +157,7 @@ const defaultBotPhoneNumberId = { } export const INTEGRATION_NAME = 'whatsapp' -export const INTEGRATION_VERSION = '4.16.0' +export const INTEGRATION_VERSION = '4.16.1' export default new IntegrationDefinition({ name: INTEGRATION_NAME, version: INTEGRATION_VERSION, diff --git a/integrations/whatsapp/src/webhook/handlers/oauth/wizard.ts b/integrations/whatsapp/src/webhook/handlers/oauth/wizard.ts index b227ab01d46..89de2dc1cf6 100644 --- a/integrations/whatsapp/src/webhook/handlers/oauth/wizard.ts +++ b/integrations/whatsapp/src/webhook/handlers/oauth/wizard.ts @@ -54,6 +54,13 @@ export const handler = async (props: bp.HandlerProps): Promise => { const _startConfirmHandler: WizardHandler = async (props) => { const { responses } = props + + // When nothing is connected yet there's nothing to reset, so skip the + // confirmation and go straight to the next step. + if (!(await _isAlreadyConnected(props))) { + return _setupHandler(props) + } + return responses.displayButtons({ pageTitle: 'Reset Configuration', htmlOrMarkdownPageContents: @@ -65,6 +72,15 @@ const _startConfirmHandler: WizardHandler = async (props) => { }) } +const _isAlreadyConnected = async ({ client, ctx }: bp.HandlerProps): Promise => { + try { + const { accessToken } = await _getCredentialsState(client, ctx) + return Boolean(accessToken) + } catch { + return false + } +} + const _setupHandler: WizardHandler = async (props) => { const { responses, client, ctx } = props // Clean current state to start a fresh wizard diff --git a/integrations/zendesk/integration.definition.ts b/integrations/zendesk/integration.definition.ts index 7457bab9940..a220711988e 100644 --- a/integrations/zendesk/integration.definition.ts +++ b/integrations/zendesk/integration.definition.ts @@ -6,7 +6,7 @@ import { actions, events, configuration, channels, states, user } from './src/de export default new sdk.IntegrationDefinition({ name: 'zendesk', title: 'Zendesk', - version: '3.1.4', + version: '3.1.5', icon: 'icon.svg', description: 'Optimize your support workflow. Trigger workflows from ticket updates as well as manage tickets, access conversations, and engage with customers.', diff --git a/integrations/zendesk/src/oauth/wizard.ts b/integrations/zendesk/src/oauth/wizard.ts index 35f9464ff90..da790b067d3 100644 --- a/integrations/zendesk/src/oauth/wizard.ts +++ b/integrations/zendesk/src/oauth/wizard.ts @@ -39,8 +39,15 @@ export const handler = async (props: bp.HandlerProps) => { return response } -const _startHandler: WizardHandler = (props) => { +const _startHandler: WizardHandler = async (props) => { const { responses } = props + + // When nothing is connected yet there's nothing to reset, so skip the + // confirmation and go straight to the next step. + if (!(await _isAlreadyConnected(props))) { + return _getSubdomain(props) + } + return responses.displayButtons({ pageTitle: 'Reset Configuration', htmlOrMarkdownPageContents: @@ -61,6 +68,15 @@ const _startHandler: WizardHandler = (props) => { }) } +const _isAlreadyConnected = async ({ client, ctx }: bp.HandlerProps): Promise => { + try { + const result = await client.getState({ type: 'integration', name: 'credentials', id: ctx.integrationId }) + return Boolean(result?.state?.payload?.accessToken) + } catch { + return false + } +} + const _getSubdomain: WizardHandler = async (props) => { const { responses } = props return responses.displayInput({ diff --git a/packages/cli/package.json b/packages/cli/package.json index 961c168aa61..6c1173965f4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,7 +28,7 @@ "@apidevtools/json-schema-ref-parser": "^11.7.0", "@botpress/chat": "0.5.5", "@botpress/client": "1.46.0", - "@botpress/sdk": "6.11.1", + "@botpress/sdk": "6.11.2", "@bpinternal/const": "^0.1.0", "@bpinternal/tunnel": "^0.1.1", "@bpinternal/verel": "^0.2.0", diff --git a/packages/cli/templates/empty-bot/package.json b/packages/cli/templates/empty-bot/package.json index ca8e6e5c92a..c450effa358 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.46.0", - "@botpress/sdk": "6.11.1" + "@botpress/sdk": "6.11.2" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-integration/package.json b/packages/cli/templates/empty-integration/package.json index 07e44d7a63e..d28248d8642 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.46.0", - "@botpress/sdk": "6.11.1" + "@botpress/sdk": "6.11.2" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-plugin/package.json b/packages/cli/templates/empty-plugin/package.json index 233249525f0..daa5d291e0b 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": "6.11.1" + "@botpress/sdk": "6.11.2" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/hello-world/package.json b/packages/cli/templates/hello-world/package.json index c76cf0095aa..56efb4f776e 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.46.0", - "@botpress/sdk": "6.11.1" + "@botpress/sdk": "6.11.2" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/webhook-message/package.json b/packages/cli/templates/webhook-message/package.json index 544869e9d4a..c3661c67b0e 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.46.0", - "@botpress/sdk": "6.11.1", + "@botpress/sdk": "6.11.2", "axios": "^1.6.8" }, "devDependencies": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 53c374c07e8..1e0128a45ce 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/sdk", - "version": "6.11.1", + "version": "6.11.2", "description": "Botpress SDK", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/sdk/src/base-logger.test.ts b/packages/sdk/src/base-logger.test.ts new file mode 100644 index 00000000000..ac5425c0de3 --- /dev/null +++ b/packages/sdk/src/base-logger.test.ts @@ -0,0 +1,95 @@ +import { describe, it, vi, afterEach } from 'vitest' +import { BaseLogger, type IssueLogEvent } from './base-logger' +import { IntegrationLogger } from './integration/server/integration-logger' + +// A minimal concrete subclass for testing BaseLogger directly: +class TestLogger extends BaseLogger { + public constructor(options: object = {}) { + super(options) + } + + public override with(_options: object) { + return new TestLogger({ ...this.defaultOptions, ..._options }) + } +} + +const MOCK_ISSUE = { + type: 'issue', + code: 'TEST_CODE', + category: 'other', + title: 'Test issue', + description: 'A test issue description', + data: {}, + groupBy: [], +} as const satisfies IssueLogEvent + +afterEach(() => vi.restoreAllMocks()) + +describe.sequential('BaseLogger.issue()', () => { + it('emits JSON with no extra keys when context is empty', ({ expect }) => { + // Arrange + const logger = new TestLogger() + const spy = vi.spyOn(console, 'info').mockImplementation(() => {}) + + // Act + logger.issue(MOCK_ISSUE) + + // Assert + const emitted = JSON.parse(spy.mock.calls[0]![0] as string) + expect(emitted).toEqual(MOCK_ISSUE) + }) + + it('emits the exact substring "type":"issue" (no spaces in JSON)', ({ expect }) => { + // Arrange + const logger = new TestLogger() + const spy = vi.spyOn(console, 'info').mockImplementation(() => {}) + + // Act + logger.issue(MOCK_ISSUE) + + // Assert + const raw = spy.mock.calls[0]![0] as string + expect(raw).toContain('"type":"issue"') + }) +}) + +describe.sequential('IntegrationLogger.issue(): with identity options', () => { + it('includes botId, integrationId, and integrationAlias at the top level', ({ expect }) => { + // Arrange + const logger = new IntegrationLogger({ + botId: 'bot-123', + integrationId: 'intg-456', + integrationAlias: 'myIntegration', + }) + const spy = vi.spyOn(console, 'info').mockImplementation(() => {}) + + // Act + logger.issue(MOCK_ISSUE) + + // Assert + const emitted = JSON.parse(spy.mock.calls[0]![0] as string) + expect(emitted.botId).toBe('bot-123') + expect(emitted.integrationId).toBe('intg-456') + expect(emitted.integrationAlias).toBe('myIntegration') + expect(emitted.type).toBe('issue') + expect(emitted.code).toBe(MOCK_ISSUE.code) + }) +}) + +describe.sequential('IntegrationLogger.issue(): without identity options', () => { + it('emits exactly the original args keys (no extra keys, none undefined)', ({ expect }) => { + // Arrange + const logger = new IntegrationLogger() + const spy = vi.spyOn(console, 'info').mockImplementation(() => {}) + + // Act + logger.issue(MOCK_ISSUE) + + // Assert + const emitted = JSON.parse(spy.mock.calls[0]![0] as string) + const emittedKeys = Object.keys(emitted).sort() + const expectedKeys = Object.keys(MOCK_ISSUE).sort() + expect(emittedKeys).toEqual(expectedKeys) + expect(Object.values(emitted).every((v) => v !== undefined)).toBe(true) + }) +}) diff --git a/packages/sdk/src/base-logger.ts b/packages/sdk/src/base-logger.ts index 32ed8480c20..4a5804572c3 100644 --- a/packages/sdk/src/base-logger.ts +++ b/packages/sdk/src/base-logger.ts @@ -40,7 +40,15 @@ export abstract class BaseLogger { } public issue(args: IssueLogEvent) { - console.info(JSON.stringify(args)) + console.info(JSON.stringify({ ...args, ...this.getIssueContext() })) + } + + /** + * Identity fields merged into every issue line so downstream ingestion can + * attribute and validate the issue without out-of-band context. + */ + protected getIssueContext(): Record { + return {} } private _log(level: LogLevel, args: Parameters) { diff --git a/packages/sdk/src/integration/server/integration-logger.ts b/packages/sdk/src/integration/server/integration-logger.ts index da9c3949cc7..b5f1f174bac 100644 --- a/packages/sdk/src/integration/server/integration-logger.ts +++ b/packages/sdk/src/integration/server/integration-logger.ts @@ -87,4 +87,13 @@ export class IntegrationLogger extends BaseLogger { options: this.defaultOptions, }) } + + protected override getIssueContext(): Record { + const { botId, integrationId, integrationAlias } = this.defaultOptions + return { + ...(botId && { botId }), + ...(integrationId && { integrationId }), + ...(integrationAlias && { integrationAlias }), + } + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb19021371d..2454443de73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2832,7 +2832,7 @@ importers: specifier: 1.46.0 version: link:../client '@botpress/sdk': - specifier: 6.11.1 + specifier: 6.11.2 version: link:../sdk '@bpinternal/const': specifier: ^0.1.0 @@ -2956,7 +2956,7 @@ importers: specifier: 1.46.0 version: link:../../../client '@botpress/sdk': - specifier: 6.11.1 + specifier: 6.11.2 version: link:../../../sdk devDependencies: '@types/node': @@ -2972,7 +2972,7 @@ importers: specifier: 1.46.0 version: link:../../../client '@botpress/sdk': - specifier: 6.11.1 + specifier: 6.11.2 version: link:../../../sdk devDependencies: '@types/node': @@ -2985,7 +2985,7 @@ importers: packages/cli/templates/empty-plugin: dependencies: '@botpress/sdk': - specifier: 6.11.1 + specifier: 6.11.2 version: link:../../../sdk devDependencies: '@types/node': @@ -3001,7 +3001,7 @@ importers: specifier: 1.46.0 version: link:../../../client '@botpress/sdk': - specifier: 6.11.1 + specifier: 6.11.2 version: link:../../../sdk devDependencies: '@types/node': @@ -3017,7 +3017,7 @@ importers: specifier: 1.46.0 version: link:../../../client '@botpress/sdk': - specifier: 6.11.1 + specifier: 6.11.2 version: link:../../../sdk axios: specifier: ^1.6.8