Skip to content

Commit ca34c72

Browse files
aadamgoughAdam Gough
andauthored
fix(tools): fixed jira and confluence token refresh (#313)
* refresh token jira and confluence * added jira bulk fetch * changed name * jira docs * updated bulk_read in docs * added bulk_read.ts * added jira_bulk_read * jira docs updated --------- Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
1 parent d410b29 commit ca34c72

9 files changed

Lines changed: 270 additions & 10 deletions

File tree

docs/content/docs/tools/jira.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,25 @@ Write a Jira issue
114114
| `success` | string |
115115
| `url` | string |
116116

117+
### `jira_bulk_read`
118+
119+
Retrieve multiple Jira issues in bulk
120+
121+
#### Input
122+
123+
| Parameter | Type | Required | Description |
124+
| --------- | ---- | -------- | ----------- |
125+
| `accessToken` | string | Yes | OAuth access token for Jira |
126+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
127+
| `projectId` | string | Yes | Jira project ID |
128+
| `cloudId` | string | No | Jira cloud ID |
129+
130+
#### Output
131+
132+
| Parameter | Type |
133+
| --------- | ---- |
134+
| `issues` | array |
135+
117136

118137

119138
## Block Configuration

sim/blocks/blocks/jira.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { JiraIcon } from '@/components/icons'
22
import { BlockConfig } from '../types'
3-
import { JiraRetrieveResponse, JiraUpdateResponse, JiraWriteResponse } from '@/tools/jira/types'
3+
import { JiraRetrieveResponse, JiraUpdateResponse, JiraWriteResponse, JiraRetrieveResponseBulk } from '@/tools/jira/types'
44

5-
type JiraResponse = JiraRetrieveResponse | JiraUpdateResponse | JiraWriteResponse
5+
type JiraResponse = JiraRetrieveResponse | JiraUpdateResponse | JiraWriteResponse | JiraRetrieveResponseBulk
66

77
export const JiraBlock: BlockConfig<JiraResponse> = {
88
type: 'jira',
@@ -22,6 +22,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
2222
layout: 'full',
2323
options: [
2424
{ label: 'Read Issue', id: 'read' },
25+
{ label: 'Read Issues', id: 'read-bulk' },
2526
{ label: 'Update Issue', id: 'update' },
2627
{ label: 'Write Issue', id: 'write' },
2728
],
@@ -60,7 +61,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
6061
provider: 'jira',
6162
serviceId: 'jira',
6263
placeholder: 'Select Jira project',
63-
condition: { field: 'operation', value: ['read', 'update', 'write'] },
6464
},
6565
{
6666
id: 'issueKey',
@@ -90,7 +90,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
9090
},
9191
],
9292
tools: {
93-
access: ['jira_retrieve', 'jira_update', 'jira_write'],
93+
access: ['jira_retrieve', 'jira_update', 'jira_write', 'jira_bulk_read'],
9494
config: {
9595
tool: (params) => {
9696
switch (params.operation) {
@@ -100,6 +100,8 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
100100
return 'jira_update'
101101
case 'write':
102102
return 'jira_write'
103+
case 'read-bulk':
104+
return 'jira_bulk_read'
103105
default:
104106
return 'jira_retrieve'
105107
}
@@ -149,6 +151,13 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
149151
issueKey: params.issueKey,
150152
}
151153
}
154+
case 'read-bulk': {
155+
// For read-bulk operations, only include read-bulk-specific fields
156+
return {
157+
...baseParams,
158+
projectId: params.projectId,
159+
}
160+
}
152161
default:
153162
return baseParams
154163
}

sim/lib/auth.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ export const auth = betterAuth({
490490
responseType: 'code',
491491
pkce: true,
492492
accessType: 'offline',
493+
authentication: 'basic',
493494
prompt: 'consent',
494495
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/confluence`,
495496
getUserInfo: async (tokens) => {
@@ -534,7 +535,6 @@ export const auth = betterAuth({
534535
clientId: process.env.JIRA_CLIENT_ID as string,
535536
clientSecret: process.env.JIRA_CLIENT_SECRET as string,
536537
authorizationUrl: 'https://auth.atlassian.com/authorize',
537-
prompt: 'consent',
538538
tokenUrl: 'https://auth.atlassian.com/oauth/token',
539539
userInfoUrl: 'https://api.atlassian.com/me',
540540
scopes: [
@@ -557,6 +557,11 @@ export const auth = betterAuth({
557557
'read:field-configuration:jira',
558558
'read:issue-details:jira'
559559
],
560+
responseType: 'code',
561+
pkce: true,
562+
accessType: 'offline',
563+
authentication: 'basic',
564+
prompt: 'consent',
560565
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/jira`,
561566
getUserInfo: async (tokens) => {
562567
try {

sim/lib/oauth.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,11 +407,13 @@ export async function refreshOAuthToken(
407407
tokenEndpoint = 'https://auth.atlassian.com/oauth/token'
408408
clientId = process.env.CONFLUENCE_CLIENT_ID
409409
clientSecret = process.env.CONFLUENCE_CLIENT_SECRET
410+
useBasicAuth = true
410411
break
411412
case 'jira':
412413
tokenEndpoint = 'https://auth.atlassian.com/oauth/token'
413414
clientId = process.env.JIRA_CLIENT_ID
414415
clientSecret = process.env.JIRA_CLIENT_SECRET
416+
useBasicAuth = true
415417
break
416418
case 'airtable':
417419
tokenEndpoint = 'https://airtable.com/oauth2/v1/token'
@@ -466,15 +468,16 @@ export async function refreshOAuthToken(
466468
} else {
467469
throw new Error('Both client ID and client secret are required for Airtable OAuth')
468470
}
469-
} else if (provider === 'x') {
470-
// Handle X differently
471+
} else if (provider === 'x' || provider === 'confluence' || provider === 'jira') {
472+
// Handle X and Atlassian services (Confluence, Jira) the same way
471473
// Confidential client - use Basic Auth
472474
const authString = `${clientId}:${clientSecret}`
473475
const basicAuth = Buffer.from(authString).toString('base64')
474476
headers['Authorization'] = `Basic ${basicAuth}`
475477

476478
// When using Basic Auth, don't include client_id in body
477479
delete bodyParams.client_id
480+
delete bodyParams.client_secret
478481
} else {
479482
// For other providers, use the general approach
480483
if (useBasicAuth) {

sim/tools/jira/bulk_read.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { ToolConfig } from '../types'
2+
import { JiraRetrieveBulkParams, JiraRetrieveResponseBulk } from './types'
3+
4+
export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrieveResponseBulk> = {
5+
id: 'jira_bulk_read',
6+
name: 'Jira Bulk Read',
7+
description: 'Retrieve multiple Jira issues in bulk',
8+
version: '1.0.0',
9+
oauth: {
10+
required: true,
11+
provider: 'jira',
12+
additionalScopes: [
13+
'read:jira-work',
14+
'read:jira-user',
15+
'read:me',
16+
'offline_access',
17+
],
18+
},
19+
params: {
20+
accessToken: {
21+
type: 'string',
22+
required: true,
23+
description: 'OAuth access token for Jira',
24+
},
25+
domain: {
26+
type: 'string',
27+
required: true,
28+
requiredForToolCall: true,
29+
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
30+
},
31+
projectId: {
32+
type: 'string',
33+
required: true,
34+
description: 'Jira project ID',
35+
},
36+
cloudId: {
37+
type: 'string',
38+
required: false,
39+
description: 'Jira cloud ID',
40+
},
41+
},
42+
request: {
43+
url: (params: JiraRetrieveBulkParams) => {
44+
if (params.cloudId) {
45+
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/picker?currentJQL=project=${params.projectId}`
46+
}
47+
// If no cloudId, use the accessible resources endpoint
48+
return 'https://api.atlassian.com/oauth/token/accessible-resources'
49+
},
50+
method: 'GET',
51+
headers: (params: JiraRetrieveBulkParams) => ({
52+
'Authorization': `Bearer ${params.accessToken}`,
53+
'Accept': 'application/json'
54+
}),
55+
body: (params: JiraRetrieveBulkParams) => ({})
56+
},
57+
transformResponse: async (response: Response, params?: JiraRetrieveBulkParams) => {
58+
if (!params) {
59+
throw new Error('Parameters are required for Jira bulk issue retrieval')
60+
}
61+
62+
try {
63+
// If we don't have a cloudId, we need to fetch it first
64+
if (!params.cloudId) {
65+
if (!response.ok) {
66+
const errorData = await response.json().catch(() => null)
67+
throw new Error(errorData?.message || `Failed to fetch accessible resources: ${response.status} ${response.statusText}`)
68+
}
69+
70+
const accessibleResources = await response.json()
71+
if (!Array.isArray(accessibleResources) || accessibleResources.length === 0) {
72+
throw new Error('No accessible Jira resources found for this account')
73+
}
74+
75+
const normalizedInput = `https://${params.domain}`.toLowerCase()
76+
const matchedResource = accessibleResources.find(r => r.url.toLowerCase() === normalizedInput)
77+
78+
if (!matchedResource) {
79+
throw new Error(`Could not find matching Jira site for domain: ${params.domain}`)
80+
}
81+
82+
// First get issue keys from picker
83+
const pickerUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/picker?currentJQL=project=${params.projectId}`
84+
const pickerResponse = await fetch(pickerUrl, {
85+
method: 'GET',
86+
headers: {
87+
'Authorization': `Bearer ${params.accessToken}`,
88+
'Accept': 'application/json'
89+
}
90+
})
91+
92+
if (!pickerResponse.ok) {
93+
const errorData = await pickerResponse.json().catch(() => null)
94+
throw new Error(errorData?.message || `Failed to retrieve issue keys: ${pickerResponse.status} ${pickerResponse.statusText}`)
95+
}
96+
97+
const pickerData = await pickerResponse.json()
98+
const issueKeys = pickerData.sections
99+
.flatMap((section: any) => section.issues || [])
100+
.map((issue: any) => issue.key)
101+
102+
if (issueKeys.length === 0) {
103+
return {
104+
success: true,
105+
output: []
106+
}
107+
}
108+
109+
// Now use bulkfetch to get the full issue details
110+
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/bulkfetch`
111+
const bulkfetchResponse = await fetch(bulkfetchUrl, {
112+
method: 'POST',
113+
headers: {
114+
'Authorization': `Bearer ${params.accessToken}`,
115+
'Accept': 'application/json',
116+
'Content-Type': 'application/json'
117+
},
118+
body: JSON.stringify({
119+
expand: ["names"],
120+
fields: ["summary", "description", "created", "updated"],
121+
fieldsByKeys: false,
122+
issueIdsOrKeys: issueKeys,
123+
properties: []
124+
})
125+
})
126+
127+
if (!bulkfetchResponse.ok) {
128+
const errorData = await bulkfetchResponse.json().catch(() => null)
129+
throw new Error(errorData?.message || `Failed to retrieve Jira issues: ${bulkfetchResponse.status} ${bulkfetchResponse.statusText}`)
130+
}
131+
132+
const data = await bulkfetchResponse.json()
133+
return {
134+
success: true,
135+
output: data.issues.map((issue: any) => ({
136+
ts: new Date().toISOString(),
137+
summary: issue.fields.summary,
138+
description: issue.fields.description?.content?.[0]?.content?.[0]?.text || '',
139+
created: issue.fields.created,
140+
updated: issue.fields.updated
141+
}))
142+
}
143+
}
144+
145+
// If we have a cloudId, this response is from the issue picker
146+
if (!response.ok) {
147+
const errorData = await response.json().catch(() => null)
148+
throw new Error(errorData?.message || `Failed to retrieve issue keys: ${response.status} ${response.statusText}`)
149+
}
150+
151+
const pickerData = await response.json()
152+
const issueKeys = pickerData.sections
153+
.flatMap((section: any) => section.issues || [])
154+
.map((issue: any) => issue.key)
155+
156+
if (issueKeys.length === 0) {
157+
return {
158+
success: true,
159+
output: []
160+
}
161+
}
162+
163+
// Use bulkfetch to get the full issue details
164+
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/bulkfetch`
165+
const bulkfetchResponse = await fetch(bulkfetchUrl, {
166+
method: 'POST',
167+
headers: {
168+
'Authorization': `Bearer ${params.accessToken}`,
169+
'Accept': 'application/json',
170+
'Content-Type': 'application/json'
171+
},
172+
body: JSON.stringify({
173+
expand: ["names"],
174+
fields: ["summary", "description", "created", "updated"],
175+
fieldsByKeys: false,
176+
issueIdsOrKeys: issueKeys,
177+
properties: []
178+
})
179+
})
180+
181+
if (!bulkfetchResponse.ok) {
182+
const errorData = await bulkfetchResponse.json().catch(() => null)
183+
throw new Error(errorData?.message || `Failed to retrieve Jira issues: ${bulkfetchResponse.status} ${bulkfetchResponse.statusText}`)
184+
}
185+
186+
const data = await bulkfetchResponse.json()
187+
return {
188+
success: true,
189+
output: data.issues.map((issue: any) => ({
190+
ts: new Date().toISOString(),
191+
summary: issue.fields.summary,
192+
description: issue.fields.description?.content?.[0]?.content?.[0]?.text || '',
193+
created: issue.fields.created,
194+
updated: issue.fields.updated
195+
}))
196+
}
197+
} catch (error) {
198+
throw error instanceof Error ? error : new Error(String(error))
199+
}
200+
},
201+
transformError: (error: any) => {
202+
return error.message || 'Failed to retrieve Jira issues'
203+
}
204+
}

sim/tools/jira/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { jiraRetrieveTool } from './retrieve'
22
import { jiraUpdateTool } from './update'
33
import { jiraWriteTool } from './write'
4+
import { jiraBulkRetrieveTool } from './bulk_read'
45

56
export { jiraRetrieveTool }
67
export { jiraUpdateTool }
78
export { jiraWriteTool }
9+
export { jiraBulkRetrieveTool }

sim/tools/jira/retrieve.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ToolConfig } from '../types'
2-
import { JiraRetrieveResponse } from './types'
3-
import { JiraRetrieveParams } from './types'
2+
import { JiraRetrieveResponse, JiraRetrieveParams } from './types'
43

54
export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveResponse> = {
65
id: 'jira_retrieve',

sim/tools/jira/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ export interface JiraRetrieveResponse extends ToolResponse {
1818
}
1919
}
2020

21+
export interface JiraRetrieveBulkParams {
22+
accessToken: string
23+
domain: string
24+
projectId: string
25+
cloudId: string
26+
}
27+
28+
export interface JiraRetrieveResponseBulk extends ToolResponse {
29+
output: {
30+
ts: string
31+
summary: string
32+
description: string
33+
created: string
34+
updated: string
35+
}[]
36+
}
37+
38+
2139
export interface JiraUpdateParams {
2240
accessToken: string
2341
domain: string

0 commit comments

Comments
 (0)