diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000000..a901602f25b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,34 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [3.0.13] - 2026-01-13
+
+### Added
+- **JIRA Tool: Bearer Token Authentication** - Support for enterprise/self-hosted JIRA instances requiring Bearer token authentication.
+- **JIRA Tool: SSL Certificate Support** - New `sslCertPath` and `sslKeyPath` options for secure connections to JIRA servers with custom certificates.
+- **JIRA Tool: SSL Verification Toggle** - `verifySslCerts` option to disable certificate verification for development environments.
+- **JIRA Tool: Comprehensive Test Suite** - Unit tests, integration tests, and regression tests for Bearer Token + SSL features.
+- **JIRA Tool: Documentation** - Added README.md with authentication guides, SSL configuration, and troubleshooting.
+
+### Changed
+- **JiraApi Credential** - Updated to version 2.0 with authentication type selector (Basic Auth / Bearer Token).
+- **JIRA Tool Core** - Refactored `BaseJiraTool` class with `buildHeaders()` and `buildFetchOptions()` methods for cleaner authentication handling.
+
+### Fixed
+- Improved error messages for SSL certificate loading failures.
+
+### Notes
+- **Backward Compatibility**: Basic Auth (`email + apiToken`) continues to work exactly as before. Existing users do not need to change anything.
+- **Migration**: Users can switch to Bearer Token by selecting "Bearer Token" in the credential configuration and providing their token.
+
+---
+
+## [3.0.12] - Previous Release
+
+_See [GitHub Releases](https://github.com/FlowiseAI/Flowise/releases) for previous version history._
diff --git a/packages/components/credentials/JiraApi.credential.ts b/packages/components/credentials/JiraApi.credential.ts
index 6638f2e0b40..6eff6cd53cb 100644
--- a/packages/components/credentials/JiraApi.credential.ts
+++ b/packages/components/credentials/JiraApi.credential.ts
@@ -10,21 +10,80 @@ class JiraApi implements INodeCredential {
constructor() {
this.label = 'Jira API'
this.name = 'jiraApi'
- this.version = 1.0
+ this.version = 2.0
this.description =
'Refer to official guide on how to get accessToken on Github'
this.inputs = [
+ {
+ label: 'Authentication Type',
+ name: 'authType',
+ type: 'options',
+ default: 'basicAuth',
+ description: 'Choose between Basic Authentication or Bearer Token',
+ options: [
+ {
+ label: 'Basic Auth (Email + API Token)',
+ name: 'basicAuth'
+ },
+ {
+ label: 'Bearer Token',
+ name: 'bearerToken'
+ }
+ ]
+ },
{
label: 'User Name',
name: 'username',
type: 'string',
- placeholder: 'username@example.com'
+ placeholder: 'username@example.com',
+ description: 'Email or username for Basic Auth',
+ show: {
+ authType: ['basicAuth']
+ }
},
{
label: 'Access Token',
name: 'accessToken',
type: 'password',
- placeholder: ''
+ placeholder: '',
+ description: 'API token for Basic Auth',
+ show: {
+ authType: ['basicAuth']
+ }
+ },
+ {
+ label: 'Bearer Token',
+ name: 'bearerTokenValue',
+ type: 'password',
+ placeholder: '',
+ description: 'Bearer token for token-based authentication',
+ show: {
+ authType: ['bearerToken']
+ }
+ },
+ {
+ label: 'SSL Certificate Path (Optional)',
+ name: 'sslCertPath',
+ type: 'string',
+ placeholder: '/path/to/cert.pem',
+ description: 'Path to the SSL certificate file (e.g., custom CA, client certificate for mTLS)',
+ optional: true
+ },
+ {
+ label: 'SSL Key Path (Optional)',
+ name: 'sslKeyPath',
+ type: 'string',
+ placeholder: '/path/to/key.pem',
+ description: 'Path to SSL key file for client certificate authentication',
+ optional: true
+ },
+ {
+ label: 'Verify SSL Certificates',
+ name: 'verifySslCerts',
+ type: 'boolean',
+ default: true,
+ description: 'Whether to verify SSL certificates (disable only for self-signed certs)',
+ optional: true
}
]
}
diff --git a/packages/components/nodes/tools/Jira/Jira.test.ts b/packages/components/nodes/tools/Jira/Jira.test.ts
new file mode 100644
index 00000000000..3b3fe444742
--- /dev/null
+++ b/packages/components/nodes/tools/Jira/Jira.test.ts
@@ -0,0 +1,889 @@
+/**
+ * Unit & Integration Tests for JIRA Tool (Bearer Token + SSL Support)
+ *
+ * Test Coverage:
+ * - Unit Tests: Authentication header generation, SSL configuration
+ * - Integration Tests: Mock JIRA server with Bearer Token and SSL
+ * - Regression Tests: Basic Auth backward compatibility
+ */
+
+import * as fs from 'fs'
+import * as https from 'https'
+
+// Mock node-fetch
+jest.mock('node-fetch', () => {
+ const mockFetch = jest.fn()
+ return {
+ __esModule: true,
+ default: mockFetch,
+ __mockFetch: mockFetch
+ }
+})
+
+// Mock fs module for SSL certificate tests
+jest.mock('fs', () => ({
+ ...jest.requireActual('fs'),
+ readFileSync: jest.fn()
+}))
+
+// Mock https module
+jest.mock('https', () => ({
+ Agent: jest.fn().mockImplementation((options) => ({
+ options,
+ _isHttpsAgent: true
+ }))
+}))
+
+// Mock utility functions
+jest.mock('../../../src/utils', () => ({
+ getBaseClasses: jest.fn(() => ['Tool', 'StructuredTool', 'DynamicStructuredTool'])
+}))
+
+jest.mock('../../../src/agents', () => ({
+ TOOL_ARGS_PREFIX: '\n---TOOL_ARGS---',
+ formatToolError: jest.fn((error) => `Error: ${error}`)
+}))
+
+describe('JIRA Tool Authentication & SSL Tests', () => {
+ let mockFetch: jest.Mock
+ let mockReadFileSync: jest.Mock
+ let mockHttpsAgent: jest.Mock
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+
+ // Get mock functions
+ const fetchModule = require('node-fetch')
+ mockFetch = fetchModule.__mockFetch
+
+ mockReadFileSync = fs.readFileSync as jest.Mock
+ mockHttpsAgent = https.Agent as jest.Mock
+
+ // Default successful response
+ mockFetch.mockResolvedValue({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: () => Promise.resolve(JSON.stringify({ id: 'PROJ-123', key: 'PROJ-123' }))
+ })
+ })
+
+ /**
+ * ===========================================
+ * UNIT TESTS: Authentication Header Generation
+ * ===========================================
+ */
+ describe('Unit Tests: Authentication Headers', () => {
+ describe('Bearer Token Authentication', () => {
+ it('should set Authorization: Bearer when bearerToken is provided', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'my-secret-bearer-token',
+ username: '',
+ accessToken: ''
+ })
+
+ // Find the getIssue tool and invoke it
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ expect(getIssueTool).toBeDefined()
+
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ // Verify fetch was called with Bearer token
+ expect(mockFetch).toHaveBeenCalledTimes(1)
+ const [url, options] = mockFetch.mock.calls[0]
+
+ expect(url).toBe('https://jira.example.com/rest/api/3/issue/PROJ-123')
+ expect(options.headers).toBeDefined()
+ expect(options.headers.Authorization).toBe('Bearer my-secret-bearer-token')
+ })
+
+ it('should use Bearer token even when email/apiToken are also provided', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'my-bearer-token',
+ username: 'user@example.com',
+ accessToken: 'api-token-should-be-ignored'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ const [, options] = mockFetch.mock.calls[0]
+ expect(options.headers.Authorization).toBe('Bearer my-bearer-token')
+ expect(options.headers.Authorization).not.toContain('Basic')
+ })
+
+ it('should handle empty bearer token gracefully', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: '',
+ username: '',
+ accessToken: ''
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ const [, options] = mockFetch.mock.calls[0]
+ // Empty bearer token should still create Bearer header (server will reject)
+ expect(options.headers.Authorization).toBe('Bearer ')
+ })
+ })
+
+ describe('Basic Authentication', () => {
+ it('should set Authorization: Basic when email + apiToken are provided', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'basicAuth',
+ username: 'user@example.com',
+ accessToken: 'my-api-token',
+ bearerToken: ''
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ const [, options] = mockFetch.mock.calls[0]
+
+ // Calculate expected Basic auth header
+ const expectedAuth = Buffer.from('user@example.com:my-api-token').toString('base64')
+ expect(options.headers.Authorization).toBe(`Basic ${expectedAuth}`)
+ })
+
+ it('should use Basic auth when authType is not specified (default)', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ username: 'user@example.com',
+ accessToken: 'my-api-token'
+ // authType not specified - should default to basicAuth
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ const [, options] = mockFetch.mock.calls[0]
+ expect(options.headers.Authorization).toContain('Basic ')
+ })
+
+ it('should handle special characters in username and password', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'basicAuth',
+ username: 'user+special@example.com',
+ accessToken: 'token:with:colons!@#$'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ const [, options] = mockFetch.mock.calls[0]
+ const expectedAuth = Buffer.from('user+special@example.com:token:with:colons!@#$').toString('base64')
+ expect(options.headers.Authorization).toBe(`Basic ${expectedAuth}`)
+ })
+ })
+
+ describe('Required Headers', () => {
+ it('should always include Content-Type and Accept headers', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token'
+ })
+
+ const createIssueTool = tools.find((t) => t.name === 'jira_create_issue')
+ await createIssueTool!.invoke({
+ projectKey: 'PROJ',
+ issueType: 'Bug',
+ summary: 'Test issue'
+ })
+
+ const [, options] = mockFetch.mock.calls[0]
+ expect(options.headers['Content-Type']).toBe('application/json')
+ expect(options.headers.Accept).toBe('application/json')
+ })
+ })
+ })
+
+ /**
+ * ===========================================
+ * UNIT TESTS: SSL Certificate Configuration
+ * ===========================================
+ */
+ describe('Unit Tests: SSL Certificate Configuration', () => {
+ describe('Client Certificate and Key', () => {
+ it('should configure HTTPS agent with certificate and key when both are provided', async () => {
+ mockReadFileSync
+ .mockReturnValueOnce('-----BEGIN CERTIFICATE-----\nCERTIFICATE_DATA\n-----END CERTIFICATE-----')
+ .mockReturnValueOnce('-----BEGIN PRIVATE KEY-----\nKEY_DATA\n-----END PRIVATE KEY-----')
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token',
+ sslCertPath: '/path/to/cert.pem',
+ sslKeyPath: '/path/to/key.pem',
+ verifySslCerts: true
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ // Verify fs.readFileSync was called for both cert and key
+ expect(mockReadFileSync).toHaveBeenCalledWith('/path/to/cert.pem', 'utf-8')
+ expect(mockReadFileSync).toHaveBeenCalledWith('/path/to/key.pem', 'utf-8')
+
+ // Verify HTTPS Agent was created with correct options
+ expect(mockHttpsAgent).toHaveBeenCalled()
+ const agentOptions = mockHttpsAgent.mock.calls[0][0]
+ expect(agentOptions.cert).toContain('CERTIFICATE_DATA')
+ expect(agentOptions.key).toContain('KEY_DATA')
+ expect(agentOptions.rejectUnauthorized).toBe(true)
+ })
+ })
+
+ describe('CA Certificate Only', () => {
+ it('should configure HTTPS agent with CA certificate when only sslCertPath is provided', async () => {
+ mockReadFileSync.mockReturnValueOnce('-----BEGIN CERTIFICATE-----\nCA_CERT_DATA\n-----END CERTIFICATE-----')
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token',
+ sslCertPath: '/path/to/ca-cert.pem',
+ // sslKeyPath not provided - indicates CA cert only
+ verifySslCerts: true
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ expect(mockReadFileSync).toHaveBeenCalledWith('/path/to/ca-cert.pem', 'utf-8')
+
+ const agentOptions = mockHttpsAgent.mock.calls[0][0]
+ expect(agentOptions.ca).toContain('CA_CERT_DATA')
+ expect(agentOptions.cert).toBeUndefined()
+ expect(agentOptions.key).toBeUndefined()
+ })
+ })
+
+ describe('SSL Verification Settings', () => {
+ it('should disable SSL verification when verifySslCerts is false', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token',
+ verifySslCerts: false
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ const agentOptions = mockHttpsAgent.mock.calls[0][0]
+ expect(agentOptions.rejectUnauthorized).toBe(false)
+ })
+
+ it('should enable SSL verification by default', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token'
+ // verifySslCerts not specified
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ const agentOptions = mockHttpsAgent.mock.calls[0][0]
+ expect(agentOptions.rejectUnauthorized).toBe(true)
+ })
+ })
+
+ describe('SSL Error Handling', () => {
+ it('should return descriptive error when certificate file cannot be read', async () => {
+ mockReadFileSync.mockImplementation(() => {
+ throw new Error('ENOENT: no such file or directory')
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token',
+ sslCertPath: '/nonexistent/cert.pem',
+ sslKeyPath: '/nonexistent/key.pem'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('Failed to load SSL certificate')
+ })
+ })
+
+ describe('HTTP vs HTTPS', () => {
+ it('should not create HTTPS agent for HTTP connections', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'http://jira.example.com', // HTTP, not HTTPS
+ authType: 'basicAuth',
+ username: 'user@example.com',
+ accessToken: 'token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ // HTTPS Agent should NOT be created for HTTP connections
+ expect(mockHttpsAgent).not.toHaveBeenCalled()
+ })
+ })
+ })
+
+ /**
+ * ===========================================
+ * INTEGRATION TESTS: Mock JIRA Server
+ * ===========================================
+ */
+ describe('Integration Tests: Mock JIRA Server', () => {
+ describe('Bearer Token Authentication Flow', () => {
+ it('should successfully authenticate with Bearer token', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: () =>
+ Promise.resolve(
+ JSON.stringify({
+ id: '10001',
+ key: 'PROJ-123',
+ fields: {
+ summary: 'Test Issue',
+ status: { name: 'Open' }
+ }
+ })
+ )
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'valid-bearer-token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ const result = await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ expect(result).toContain('PROJ-123')
+ expect(result).toContain('Test Issue')
+ })
+
+ it('should fail with 401 when Bearer token is invalid', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 401,
+ statusText: 'Unauthorized',
+ text: () => Promise.resolve('{"errorMessages":["Client must be authenticated to access this resource."]}')
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'invalid-token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('Jira API Error 401')
+ })
+
+ it('should fail with 403 when Bearer token lacks permissions', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 403,
+ statusText: 'Forbidden',
+ text: () => Promise.resolve('{"errorMessages":["You do not have permission to view this issue."]}')
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'limited-token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('Jira API Error 403')
+ })
+ })
+
+ describe('SSL Certificate Validation Flow', () => {
+ it('should succeed when correct SSL certificates are provided', async () => {
+ mockReadFileSync
+ .mockReturnValueOnce('-----BEGIN CERTIFICATE-----\nVALID_CERT\n-----END CERTIFICATE-----')
+ .mockReturnValueOnce('-----BEGIN PRIVATE KEY-----\nVALID_KEY\n-----END PRIVATE KEY-----')
+
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: () => Promise.resolve(JSON.stringify({ id: '10001', key: 'PROJ-123' }))
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://secure-jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token',
+ sslCertPath: '/path/to/valid-cert.pem',
+ sslKeyPath: '/path/to/valid-key.pem'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ const result = await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ expect(result).toContain('PROJ-123')
+ expect(mockHttpsAgent).toHaveBeenCalled()
+ })
+
+ it('should handle SSL certificate expiration errors', async () => {
+ mockReadFileSync
+ .mockReturnValueOnce('-----BEGIN CERTIFICATE-----\nEXPIRED_CERT\n-----END CERTIFICATE-----')
+ .mockReturnValueOnce('-----BEGIN PRIVATE KEY-----\nVALID_KEY\n-----END PRIVATE KEY-----')
+
+ mockFetch.mockRejectedValueOnce(new Error('CERT_HAS_EXPIRED'))
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://secure-jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token',
+ sslCertPath: '/path/to/expired-cert.pem',
+ sslKeyPath: '/path/to/key.pem'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('CERT_HAS_EXPIRED')
+ })
+
+ it('should handle self-signed certificate with verifySslCerts disabled', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: () => Promise.resolve(JSON.stringify({ id: '10001', key: 'PROJ-123' }))
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://self-signed-jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token',
+ verifySslCerts: false
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ const result = await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ expect(result).toContain('PROJ-123')
+
+ const agentOptions = mockHttpsAgent.mock.calls[0][0]
+ expect(agentOptions.rejectUnauthorized).toBe(false)
+ })
+ })
+
+ describe('Full CRUD Operations', () => {
+ it('should create issue with Bearer token', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 201,
+ statusText: 'Created',
+ text: () =>
+ Promise.resolve(
+ JSON.stringify({
+ id: '10002',
+ key: 'PROJ-124',
+ self: 'https://jira.example.com/rest/api/3/issue/10002'
+ })
+ )
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'create-token'
+ })
+
+ const createIssueTool = tools.find((t) => t.name === 'jira_create_issue')
+ const result = await createIssueTool!.invoke({
+ projectKey: 'PROJ',
+ issueType: 'Bug',
+ summary: 'New Bug Report',
+ description: 'This is a bug description'
+ })
+
+ expect(result).toContain('PROJ-124')
+
+ // Verify the request body
+ const [, options] = mockFetch.mock.calls[0]
+ expect(options.method).toBe('POST')
+ const body = JSON.parse(options.body)
+ expect(body.fields.summary).toBe('New Bug Report')
+ })
+
+ it('should update issue with Bearer token', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 204,
+ statusText: 'No Content',
+ text: () => Promise.resolve('')
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'update-token'
+ })
+
+ const updateIssueTool = tools.find((t) => t.name === 'jira_update_issue')
+ await updateIssueTool!.invoke({
+ issueKey: 'PROJ-123',
+ summary: 'Updated Summary'
+ })
+
+ const [url, options] = mockFetch.mock.calls[0]
+ expect(url).toBe('https://jira.example.com/rest/api/3/issue/PROJ-123')
+ expect(options.method).toBe('PUT')
+ })
+ })
+ })
+
+ /**
+ * ===========================================
+ * REGRESSION TESTS: Basic Auth Backward Compatibility
+ * ===========================================
+ */
+ describe('Regression Tests: Basic Auth Backward Compatibility', () => {
+ it('should continue to work with Basic Auth exactly as before', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: () =>
+ Promise.resolve(
+ JSON.stringify({
+ issues: [
+ { key: 'PROJ-1', fields: { summary: 'Issue 1' } },
+ { key: 'PROJ-2', fields: { summary: 'Issue 2' } }
+ ],
+ total: 2
+ })
+ )
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ // Original Basic Auth configuration
+ const tools = createJiraTools({
+ jiraHost: 'https://company.atlassian.net',
+ authType: 'basicAuth',
+ username: 'admin@company.com',
+ accessToken: 'atlassian-api-token-123'
+ })
+
+ const listIssuesTool = tools.find((t) => t.name === 'jira_list_issues')
+ const result = await listIssuesTool!.invoke({ projectKey: 'PROJ' })
+
+ expect(result).toContain('PROJ-1')
+ expect(result).toContain('PROJ-2')
+
+ // Verify Basic auth header format
+ const [, options] = mockFetch.mock.calls[0]
+ expect(options.headers.Authorization).toMatch(/^Basic [A-Za-z0-9+/=]+$/)
+
+ // Decode and verify credentials
+ const authHeader = options.headers.Authorization
+ const base64 = authHeader.replace('Basic ', '')
+ const decoded = Buffer.from(base64, 'base64').toString('utf-8')
+ expect(decoded).toBe('admin@company.com:atlassian-api-token-123')
+ })
+
+ it('should handle all existing issue operations with Basic Auth', async () => {
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://company.atlassian.net',
+ authType: 'basicAuth',
+ username: 'user@company.com',
+ accessToken: 'api-token'
+ })
+
+ // Verify all expected tools are created
+ const expectedTools = [
+ 'jira_list_issues',
+ 'jira_create_issue',
+ 'jira_get_issue',
+ 'jira_update_issue',
+ 'jira_assign_issue',
+ 'jira_transition_issue',
+ 'jira_list_comments',
+ 'jira_create_comment',
+ 'jira_get_comment',
+ 'jira_update_comment',
+ 'jira_delete_comment',
+ 'jira_search_users',
+ 'jira_get_user',
+ 'jira_create_user'
+ ]
+
+ for (const toolName of expectedTools) {
+ const tool = tools.find((t) => t.name === toolName)
+ expect(tool).toBeDefined()
+ }
+ })
+
+ it('should fetch single issue with Basic Auth (regression)', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: () =>
+ Promise.resolve(
+ JSON.stringify({
+ id: '10001',
+ key: 'PROJ-123',
+ fields: {
+ summary: 'Original Issue',
+ description: 'Original description',
+ status: { name: 'To Do' },
+ priority: { name: 'Medium' }
+ }
+ })
+ )
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://company.atlassian.net',
+ authType: 'basicAuth',
+ username: 'user@company.com',
+ accessToken: 'api-token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ const result = await getIssueTool!.invoke({ issueKey: 'PROJ-123' })
+
+ expect(result).toContain('PROJ-123')
+ expect(result).toContain('Original Issue')
+
+ // Verify correct endpoint was called
+ const [url] = mockFetch.mock.calls[0]
+ expect(url).toBe('https://company.atlassian.net/rest/api/3/issue/PROJ-123')
+ })
+
+ it('should add comment with Basic Auth (regression)', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 201,
+ statusText: 'Created',
+ text: () =>
+ Promise.resolve(
+ JSON.stringify({
+ id: '10001',
+ body: {
+ type: 'doc',
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Test comment' }] }]
+ }
+ })
+ )
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://company.atlassian.net',
+ authType: 'basicAuth',
+ username: 'user@company.com',
+ accessToken: 'api-token'
+ })
+
+ const createCommentTool = tools.find((t) => t.name === 'jira_create_comment')
+ await createCommentTool!.invoke({
+ issueKey: 'PROJ-123',
+ text: 'Test comment'
+ })
+
+ const [url, options] = mockFetch.mock.calls[0]
+ expect(url).toBe('https://company.atlassian.net/rest/api/3/issue/PROJ-123/comment')
+ expect(options.method).toBe('POST')
+ })
+
+ it('should search users with Basic Auth (regression)', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: () =>
+ Promise.resolve(
+ JSON.stringify([
+ { accountId: '123', displayName: 'John Doe', emailAddress: 'john@company.com' },
+ { accountId: '456', displayName: 'Jane Doe', emailAddress: 'jane@company.com' }
+ ])
+ )
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://company.atlassian.net',
+ authType: 'basicAuth',
+ username: 'admin@company.com',
+ accessToken: 'api-token'
+ })
+
+ const searchUsersTool = tools.find((t) => t.name === 'jira_search_users')
+ const result = await searchUsersTool!.invoke({ query: 'doe' })
+
+ expect(result).toContain('John Doe')
+ expect(result).toContain('Jane Doe')
+ })
+ })
+
+ /**
+ * ===========================================
+ * ERROR HANDLING TESTS
+ * ===========================================
+ */
+ describe('Error Handling', () => {
+ it('should handle network errors gracefully', async () => {
+ mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'))
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('Failed to connect to Jira')
+ })
+
+ it('should handle timeout errors', async () => {
+ mockFetch.mockRejectedValueOnce(new Error('ETIMEDOUT'))
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('ETIMEDOUT')
+ })
+
+ it('should handle DNS resolution errors', async () => {
+ mockFetch.mockRejectedValueOnce(new Error('ENOTFOUND'))
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://nonexistent-jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('ENOTFOUND')
+ })
+
+ it('should handle rate limiting (429) errors', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 429,
+ statusText: 'Too Many Requests',
+ text: () => Promise.resolve('{"errorMessages":["Rate limit exceeded. Please retry later."]}')
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('Jira API Error 429')
+ })
+
+ it('should handle server errors (500)', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ statusText: 'Internal Server Error',
+ text: () => Promise.resolve('Internal server error occurred')
+ })
+
+ const { createJiraTools } = await import('./core')
+
+ const tools = createJiraTools({
+ jiraHost: 'https://jira.example.com',
+ authType: 'bearerToken',
+ bearerToken: 'token'
+ })
+
+ const getIssueTool = tools.find((t) => t.name === 'jira_get_issue')
+ await expect(getIssueTool!.invoke({ issueKey: 'PROJ-123' }))
+ .resolves.toContain('Jira API Error 500')
+ })
+ })
+})
diff --git a/packages/components/nodes/tools/Jira/Jira.ts b/packages/components/nodes/tools/Jira/Jira.ts
index 95c2b8c045b..649cb1ed3fd 100644
--- a/packages/components/nodes/tools/Jira/Jira.ts
+++ b/packages/components/nodes/tools/Jira/Jira.ts
@@ -372,22 +372,44 @@ class Jira_Tools implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
- const username = getCredentialParam('username', credentialData, nodeData)
- const accessToken = getCredentialParam('accessToken', credentialData, nodeData)
+ const authType = getCredentialParam('authType', credentialData, nodeData) || 'basicAuth'
const jiraHost = nodeData.inputs?.jiraHost as string
- if (!username) {
- throw new Error('No username found in credential')
+ if (!jiraHost) {
+ throw new Error('No Jira host provided')
}
- if (!accessToken) {
- throw new Error('No access token found in credential')
- }
+ let username: string | undefined
+ let accessToken: string | undefined
+ let bearerToken: string | undefined
- if (!jiraHost) {
- throw new Error('No Jira host provided')
+ // Handle authentication based on type
+ if (authType === 'basicAuth') {
+ username = getCredentialParam('username', credentialData, nodeData)
+ accessToken = getCredentialParam('accessToken', credentialData, nodeData)
+
+ if (!username) {
+ throw new Error('No username found in credential')
+ }
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+ } else if (authType === 'bearerToken') {
+ bearerToken = getCredentialParam('bearerTokenValue', credentialData, nodeData)
+
+ if (!bearerToken) {
+ throw new Error('No bearer token found in credential')
+ }
+ } else {
+ throw new Error('Invalid authentication type')
}
+ // Get SSL configuration
+ const sslCertPath = getCredentialParam('sslCertPath', credentialData, nodeData)
+ const sslKeyPath = getCredentialParam('sslKeyPath', credentialData, nodeData)
+ const verifySslCerts = getCredentialParam('verifySslCerts', credentialData, nodeData) !== false
+
// Get all actions based on type
const jiraType = nodeData.inputs?.jiraType as string
let actions: string[] = []
@@ -407,7 +429,12 @@ class Jira_Tools implements INode {
actions,
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
defaultParams
})
diff --git a/packages/components/nodes/tools/Jira/README.md b/packages/components/nodes/tools/Jira/README.md
new file mode 100644
index 00000000000..2e3a18fdbfd
--- /dev/null
+++ b/packages/components/nodes/tools/Jira/README.md
@@ -0,0 +1,249 @@
+# JIRA Tool
+
+The JIRA Tool allows you to interact with JIRA's REST API for managing issues, comments, and users directly from your Flowise workflows.
+
+## Features
+
+- **Issue Management**: Create, read, update, delete, assign, and transition issues
+- **Comment Management**: Add, view, update, and delete comments on issues
+- **User Management**: Search, view, and manage JIRA users
+- **Flexible Authentication**: Supports both Basic Auth and Bearer Token authentication
+- **SSL Certificate Support**: Connect to self-hosted JIRA instances with custom SSL certificates
+
+---
+
+## Authentication Options
+
+### 1. Basic Authentication (Default)
+
+Use this for **Atlassian Cloud** (e.g., `yourcompany.atlassian.net`) or self-hosted instances that support Basic Auth.
+
+| Field | Description |
+|-------|-------------|
+| `Email` | Your JIRA account email address |
+| `API Token` | API token generated from [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens) |
+
+**Example Configuration:**
+```json
+{
+ "baseUrl": "https://yourcompany.atlassian.net",
+ "email": "user@yourcompany.com",
+ "apiToken": "YOUR_API_TOKEN"
+}
+```
+
+### 2. Bearer Token Authentication (Enterprise/Self-Hosted)
+
+Use this for **enterprise or self-hosted JIRA instances** that require Bearer token authentication (e.g., JIRA Data Center, JIRA Server with OAuth).
+
+| Field | Description |
+|-------|-------------|
+| `Bearer Token` | Personal Access Token (PAT) or OAuth access token |
+
+**Example Configuration:**
+```json
+{
+ "baseUrl": "https://jira.mycompany.com",
+ "bearerToken": "YOUR_BEARER_TOKEN"
+}
+```
+
+> **Note:** When `bearerToken` is provided and `authType` is set to `bearerToken`, Basic Auth credentials are ignored.
+
+---
+
+## SSL Certificate Support
+
+For self-hosted JIRA instances using custom SSL certificates (self-signed or internal CA), you can configure SSL certificate paths.
+
+### Configuration Options
+
+| Field | Description |
+|-------|-------------|
+| `SSL Certificate Path` | Path to the SSL certificate file (`.pem`, `.crt`) |
+| `SSL Key Path` | Path to the private key file (optional, for mutual TLS) |
+| `Verify SSL Certificates` | Enable/disable SSL certificate verification (default: `true`) |
+
+### Usage Scenarios
+
+#### Scenario 1: Custom CA Certificate
+
+When your JIRA instance uses a certificate signed by an internal Certificate Authority:
+
+```json
+{
+ "baseUrl": "https://jira.mycompany.com",
+ "bearerToken": "YOUR_BEARER_TOKEN",
+ "sslCertPath": "/path/to/ca-cert.pem"
+}
+```
+
+#### Scenario 2: Client Certificate (Mutual TLS)
+
+When your JIRA instance requires client certificate authentication:
+
+```json
+{
+ "baseUrl": "https://jira.mycompany.com",
+ "bearerToken": "YOUR_BEARER_TOKEN",
+ "sslCertPath": "/path/to/client-cert.pem",
+ "sslKeyPath": "/path/to/client-key.pem"
+}
+```
+
+#### Scenario 3: Self-Signed Certificate (Development Only)
+
+⚠️ **Not recommended for production!** Disable SSL verification for testing purposes:
+
+```json
+{
+ "baseUrl": "https://jira.dev.local",
+ "bearerToken": "YOUR_BEARER_TOKEN",
+ "verifySslCerts": false
+}
+```
+
+---
+
+## Backward Compatibility
+
+### Existing Basic Auth Users
+
+**No changes required!** If you're currently using Basic Auth (email + API token), your configuration will continue to work without modification.
+
+| Scenario | Behavior |
+|----------|----------|
+| `bearerToken` not provided | Falls back to Basic Auth using `email` + `apiToken` |
+| `authType` set to `basicAuth` | Uses Basic Auth regardless of other fields |
+| `authType` set to `bearerToken` | Uses Bearer Token authentication |
+
+### Migration Guide
+
+To migrate from Basic Auth to Bearer Token:
+
+1. Generate a Personal Access Token (PAT) in your JIRA instance
+2. Update your credential configuration:
+ - Set `authType` to `bearerToken`
+ - Add your `bearerToken` value
+3. (Optional) Add SSL certificate paths if required
+4. Test the connection before deploying
+
+---
+
+## Error Handling
+
+### Common Errors
+
+| Error | Cause | Solution |
+|-------|-------|----------|
+| `401 Unauthorized` | Invalid credentials | Verify your API token or Bearer token is correct |
+| `403 Forbidden` | Insufficient permissions | Check your JIRA user permissions |
+| `Failed to load SSL certificate` | Certificate file not found | Verify the certificate path and file permissions |
+| `CERT_HAS_EXPIRED` | SSL certificate expired | Renew your SSL certificate |
+| `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | Self-signed certificate | Add the CA cert to `sslCertPath` or set `verifySslCerts: false` |
+
+### SSL Certificate Troubleshooting
+
+1. **File Permissions**: Ensure the certificate files are readable by the Flowise process
+ ```bash
+ chmod 644 /path/to/cert.pem
+ chmod 600 /path/to/key.pem # Private key should be more restricted
+ ```
+
+2. **Certificate Format**: Use PEM format for certificates
+ ```bash
+ # Convert from DER to PEM if needed
+ openssl x509 -inform DER -in cert.der -out cert.pem
+ ```
+
+3. **Certificate Chain**: If using intermediate CAs, include the full chain in your certificate file
+
+---
+
+## Testing
+
+### Pre-Deployment Checklist
+
+1. ✅ **Verify Connectivity**: Test that your JIRA instance is reachable from the Flowise server
+2. ✅ **Validate Credentials**: Ensure your Bearer token or API token is valid and not expired
+3. ✅ **Check SSL Configuration**: If using custom certificates, verify they are correctly configured
+4. ✅ **Test Operations**: Run a simple read operation (e.g., `Get Issue`) before deploying
+
+### Regression Testing for Basic Auth
+
+If you've updated to the new version with Bearer Token support, verify that Basic Auth still works:
+
+```javascript
+// Test Basic Auth configuration
+const config = {
+ baseUrl: "https://yourcompany.atlassian.net",
+ authType: "basicAuth",
+ email: "user@yourcompany.com",
+ apiToken: "YOUR_API_TOKEN"
+};
+
+// Verify issue retrieval works
+// GET /rest/api/3/issue/PROJ-123
+```
+
+### Testing Bearer Token
+
+```javascript
+// Test Bearer Token configuration
+const config = {
+ baseUrl: "https://jira.mycompany.com",
+ authType: "bearerToken",
+ bearerToken: "YOUR_BEARER_TOKEN",
+ sslCertPath: "/path/to/cert.pem"
+};
+
+// Verify issue retrieval works
+// GET /rest/api/3/issue/PROJ-123
+```
+
+---
+
+## Available Actions
+
+### Issue Actions
+- `List Issues` - Query issues using JQL
+- `Create Issue` - Create a new issue
+- `Get Issue` - Retrieve issue details
+- `Update Issue` - Modify issue fields
+- `Delete Issue` - Remove an issue
+- `Assign Issue` - Assign issue to a user
+- `Transition Issue` - Change issue status
+
+### Comment Actions
+- `List Comments` - Get all comments on an issue
+- `Create Comment` - Add a comment to an issue
+- `Get Comment` - Retrieve a specific comment
+- `Update Comment` - Edit a comment
+- `Delete Comment` - Remove a comment
+
+### User Actions
+- `Search Users` - Find users by query
+- `Get User` - Retrieve user details
+- `Create User` - Create a new user
+
+---
+
+## API Reference
+
+This tool uses the JIRA REST API v3. For detailed API documentation, see:
+- [Atlassian JIRA REST API Documentation](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/)
+- [JIRA Server/Data Center REST API](https://docs.atlassian.com/software/jira/docs/api/REST/latest/)
+
+---
+
+## Changelog
+
+### v2.0 (Current)
+- ✨ Added Bearer Token authentication support
+- ✨ Added SSL certificate configuration options
+- ✨ Added `verifySslCerts` option for development environments
+- 🔄 Backward compatible with existing Basic Auth configurations
+
+### v1.0
+- Initial release with Basic Auth support
+- Issue, Comment, and User management operations
diff --git a/packages/components/nodes/tools/Jira/core.ts b/packages/components/nodes/tools/Jira/core.ts
index 07cb078c539..b663757102f 100644
--- a/packages/components/nodes/tools/Jira/core.ts
+++ b/packages/components/nodes/tools/Jira/core.ts
@@ -1,5 +1,7 @@
import { z } from 'zod'
import fetch from 'node-fetch'
+import * as fs from 'fs'
+import * as https from 'https'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
import { TOOL_ARGS_PREFIX, formatToolError } from '../../../src/agents'
@@ -23,7 +25,12 @@ export interface RequestParameters {
actions?: string[]
username?: string
accessToken?: string
+ bearerToken?: string
+ authType?: string
jiraHost?: string
+ sslCertPath?: string
+ sslKeyPath?: string
+ verifySslCerts?: boolean
defaultParams?: any
}
@@ -134,13 +141,74 @@ const DeleteUserSchema = z.object({
class BaseJiraTool extends DynamicStructuredTool {
protected username: string = ''
protected accessToken: string = ''
+ protected bearerToken: string = ''
+ protected authType: string = 'basicAuth'
protected jiraHost: string = ''
+ protected sslCertPath?: string
+ protected sslKeyPath?: string
+ protected verifySslCerts: boolean = true
constructor(args: any) {
super(args)
this.username = args.username ?? ''
this.accessToken = args.accessToken ?? ''
+ this.bearerToken = args.bearerToken ?? ''
+ this.authType = args.authType ?? 'basicAuth'
this.jiraHost = args.jiraHost ?? ''
+ this.sslCertPath = args.sslCertPath
+ this.sslKeyPath = args.sslKeyPath
+ this.verifySslCerts = args.verifySslCerts !== false
+ }
+
+ private buildHeaders(): Record {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json'
+ }
+
+ // Add authentication header based on auth type
+ if (this.authType === 'basicAuth') {
+ const auth = Buffer.from(`${this.username}:${this.accessToken}`).toString('base64')
+ headers.Authorization = `Basic ${auth}`
+ } else if (this.authType === 'bearerToken') {
+ headers.Authorization = `Bearer ${this.bearerToken}`
+ }
+
+ return headers
+ }
+
+ private buildFetchOptions(): RequestInit {
+ const options: RequestInit = {}
+
+ // Add SSL configuration if needed
+ if (this.jiraHost.startsWith('https://')) {
+ const httpsAgent: any = {
+ rejectUnauthorized: this.verifySslCerts
+ }
+
+ // Load client certificate and key if provided
+ if (this.sslCertPath && this.sslKeyPath) {
+ try {
+ httpsAgent.cert = fs.readFileSync(this.sslCertPath, 'utf-8')
+ httpsAgent.key = fs.readFileSync(this.sslKeyPath, 'utf-8')
+ } catch (error) {
+ throw new Error(`Failed to load SSL certificate/key: ${error}`)
+ }
+ }
+
+ // Load CA certificate if provided
+ if (this.sslCertPath && !this.sslKeyPath) {
+ try {
+ httpsAgent.ca = fs.readFileSync(this.sslCertPath, 'utf-8')
+ } catch (error) {
+ throw new Error(`Failed to load SSL certificate: ${error}`)
+ }
+ }
+
+ options.agent = new https.Agent(httpsAgent)
+ }
+
+ return options
}
async makeJiraRequest({
@@ -155,28 +223,34 @@ class BaseJiraTool extends DynamicStructuredTool {
params?: any
}): Promise {
const url = `${this.jiraHost}/rest/api/3/${endpoint}`
- const auth = Buffer.from(`${this.username}:${this.accessToken}`).toString('base64')
+ const headers = this.buildHeaders()
+ const fetchOptions = this.buildFetchOptions()
- const headers = {
- Authorization: `Basic ${auth}`,
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- ...this.headers
- }
+ try {
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined,
+ ...fetchOptions
+ })
- const response = await fetch(url, {
- method,
- headers,
- body: body ? JSON.stringify(body) : undefined
- })
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Jira API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`Jira API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ } catch (error) {
+ if (error instanceof Error) {
+ // Don't re-wrap API errors, only wrap network/connection errors
+ if (error.message.startsWith('Jira API Error')) {
+ throw error
+ }
+ throw new Error(`Failed to connect to Jira: ${error.message}`)
+ }
+ throw error
}
-
- const data = await response.text()
- return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
}
}
@@ -197,7 +271,12 @@ class ListIssuesTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -243,7 +322,12 @@ class CreateIssueTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -323,7 +407,12 @@ class GetIssueTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -358,7 +447,12 @@ class UpdateIssueTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -426,7 +520,12 @@ class DeleteIssueTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -461,7 +560,12 @@ class AssignIssueTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -500,7 +604,12 @@ class TransitionIssueTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -542,7 +651,12 @@ class ListCommentsTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -582,7 +696,12 @@ class CreateCommentTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -639,7 +758,12 @@ class GetCommentTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -674,7 +798,12 @@ class UpdateCommentTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -727,7 +856,12 @@ class DeleteCommentTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -763,7 +897,12 @@ class SearchUsersTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -804,7 +943,12 @@ class GetUserTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -843,7 +987,12 @@ class CreateUserTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -887,7 +1036,12 @@ class UpdateUserTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -930,7 +1084,12 @@ class DeleteUserTool extends BaseJiraTool {
...toolInput,
username: args.username,
accessToken: args.accessToken,
+ bearerToken: args.bearerToken,
+ authType: args.authType,
jiraHost: args.jiraHost,
+ sslCertPath: args.sslCertPath,
+ sslKeyPath: args.sslKeyPath,
+ verifySslCerts: args.verifySslCerts,
maxOutputLength: args.maxOutputLength
})
this.defaultParams = args.defaultParams || {}
@@ -957,7 +1116,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
const actions = args?.actions || []
const username = args?.username || ''
const accessToken = args?.accessToken || ''
+ const bearerToken = args?.bearerToken || ''
+ const authType = args?.authType || 'basicAuth'
const jiraHost = args?.jiraHost || ''
+ const sslCertPath = args?.sslCertPath
+ const sslKeyPath = args?.sslKeyPath
+ const verifySslCerts = args?.verifySslCerts !== false
const maxOutputLength = args?.maxOutputLength || Infinity
const defaultParams = args?.defaultParams || {}
@@ -967,7 +1131,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new ListIssuesTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -979,7 +1148,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new CreateIssueTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -991,7 +1165,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new GetIssueTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1003,7 +1182,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new UpdateIssueTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1015,7 +1199,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new DeleteIssueTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1027,7 +1216,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new AssignIssueTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1039,7 +1233,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new TransitionIssueTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1052,7 +1251,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new ListCommentsTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1064,7 +1268,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new CreateCommentTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1076,7 +1285,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new GetCommentTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1088,7 +1302,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new UpdateCommentTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1100,7 +1319,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new DeleteCommentTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1113,7 +1337,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new SearchUsersTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1125,7 +1354,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new GetUserTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1137,7 +1371,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new CreateUserTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1149,7 +1388,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new UpdateUserTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})
@@ -1161,7 +1405,12 @@ export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool
new DeleteUserTool({
username,
accessToken,
+ bearerToken,
+ authType,
jiraHost,
+ sslCertPath,
+ sslKeyPath,
+ verifySslCerts,
maxOutputLength,
defaultParams
})