diff --git a/apps/sim/app/api/tools/jsm/approvals/route.ts b/apps/sim/app/api/tools/jsm/approvals/route.ts index 280cd4f2b5..896e5bab9d 100644 --- a/apps/sim/app/api/tools/jsm/approvals/route.ts +++ b/apps/sim/app/api/tools/jsm/approvals/route.ts @@ -1,11 +1,20 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { + validateAlphanumericId, + validateEnum, + validateJiraCloudId, + validateJiraIssueKey, +} from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' const logger = createLogger('JsmApprovalsAPI') +const VALID_ACTIONS = ['get', 'answer'] as const +const VALID_DECISIONS = ['approve', 'decline'] as const + export async function POST(request: Request) { try { const body = await request.json() @@ -41,7 +50,23 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Action is required' }, { status: 400 }) } + const actionValidation = validateEnum(action, VALID_ACTIONS, 'action') + if (!actionValidation.isValid) { + return NextResponse.json({ error: actionValidation.error }, { status: 400 }) + } + const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey') + if (!issueIdOrKeyValidation.isValid) { + return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) if (action === 'get') { @@ -91,12 +116,14 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Approval ID is required' }, { status: 400 }) } - if (!decision || !['approve', 'decline'].includes(decision)) { - logger.error('Invalid or missing decision in request') - return NextResponse.json( - { error: 'Decision is required and must be "approve" or "decline"' }, - { status: 400 } - ) + const approvalIdValidation = validateAlphanumericId(approvalId, 'approvalId') + if (!approvalIdValidation.isValid) { + return NextResponse.json({ error: approvalIdValidation.error }, { status: 400 }) + } + + const decisionValidation = validateEnum(decision, VALID_DECISIONS, 'decision') + if (!decisionValidation.isValid) { + return NextResponse.json({ error: decisionValidation.error }, { status: 400 }) } const url = `${baseUrl}/request/${issueIdOrKey}/approval/${approvalId}` diff --git a/apps/sim/app/api/tools/jsm/comment/route.ts b/apps/sim/app/api/tools/jsm/comment/route.ts index 1fa11e2234..b93b90ecce 100644 --- a/apps/sim/app/api/tools/jsm/comment/route.ts +++ b/apps/sim/app/api/tools/jsm/comment/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -38,6 +39,17 @@ export async function POST(request: Request) { } const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey') + if (!issueIdOrKeyValidation.isValid) { + return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const url = `${baseUrl}/request/${issueIdOrKey}/comment` diff --git a/apps/sim/app/api/tools/jsm/comments/route.ts b/apps/sim/app/api/tools/jsm/comments/route.ts index 06cd790230..429857f7c2 100644 --- a/apps/sim/app/api/tools/jsm/comments/route.ts +++ b/apps/sim/app/api/tools/jsm/comments/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -36,6 +37,17 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey') + if (!issueIdOrKeyValidation.isValid) { + return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const params = new URLSearchParams() diff --git a/apps/sim/app/api/tools/jsm/customers/route.ts b/apps/sim/app/api/tools/jsm/customers/route.ts index 857f4c3b58..de735337dc 100644 --- a/apps/sim/app/api/tools/jsm/customers/route.ts +++ b/apps/sim/app/api/tools/jsm/customers/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -36,6 +37,17 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId') + if (!serviceDeskIdValidation.isValid) { + return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const parsedEmails = emails diff --git a/apps/sim/app/api/tools/jsm/organization/route.ts b/apps/sim/app/api/tools/jsm/organization/route.ts index a3a3cc7c50..168a76be8a 100644 --- a/apps/sim/app/api/tools/jsm/organization/route.ts +++ b/apps/sim/app/api/tools/jsm/organization/route.ts @@ -1,11 +1,18 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { + validateAlphanumericId, + validateEnum, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' const logger = createLogger('JsmOrganizationAPI') +const VALID_ACTIONS = ['create', 'add_to_service_desk'] as const + export async function POST(request: Request) { try { const body = await request.json() @@ -34,7 +41,18 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Action is required' }, { status: 400 }) } + const actionValidation = validateEnum(action, VALID_ACTIONS, 'action') + if (!actionValidation.isValid) { + return NextResponse.json({ error: actionValidation.error }, { status: 400 }) + } + const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) if (action === 'create') { @@ -90,6 +108,16 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Organization ID is required' }, { status: 400 }) } + const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId') + if (!serviceDeskIdValidation.isValid) { + return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) + } + + const organizationIdValidation = validateAlphanumericId(organizationId, 'organizationId') + if (!organizationIdValidation.isValid) { + return NextResponse.json({ error: organizationIdValidation.error }, { status: 400 }) + } + const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization` logger.info('Adding organization to service desk:', { serviceDeskId, organizationId }) diff --git a/apps/sim/app/api/tools/jsm/organizations/route.ts b/apps/sim/app/api/tools/jsm/organizations/route.ts index 7fc53d6a66..b6b0f04001 100644 --- a/apps/sim/app/api/tools/jsm/organizations/route.ts +++ b/apps/sim/app/api/tools/jsm/organizations/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -27,6 +28,17 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId') + if (!serviceDeskIdValidation.isValid) { + return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const params = new URLSearchParams() diff --git a/apps/sim/app/api/tools/jsm/participants/route.ts b/apps/sim/app/api/tools/jsm/participants/route.ts index 2e5e6de9b6..649edd29b7 100644 --- a/apps/sim/app/api/tools/jsm/participants/route.ts +++ b/apps/sim/app/api/tools/jsm/participants/route.ts @@ -1,11 +1,18 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { + validateEnum, + validateJiraCloudId, + validateJiraIssueKey, +} from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' const logger = createLogger('JsmParticipantsAPI') +const VALID_ACTIONS = ['get', 'add'] as const + export async function POST(request: Request) { try { const body = await request.json() @@ -40,7 +47,23 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Action is required' }, { status: 400 }) } + const actionValidation = validateEnum(action, VALID_ACTIONS, 'action') + if (!actionValidation.isValid) { + return NextResponse.json({ error: actionValidation.error }, { status: 400 }) + } + const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey') + if (!issueIdOrKeyValidation.isValid) { + return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) if (action === 'get') { diff --git a/apps/sim/app/api/tools/jsm/queues/route.ts b/apps/sim/app/api/tools/jsm/queues/route.ts index c69bea1407..691de5d9cf 100644 --- a/apps/sim/app/api/tools/jsm/queues/route.ts +++ b/apps/sim/app/api/tools/jsm/queues/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -35,6 +36,17 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId') + if (!serviceDeskIdValidation.isValid) { + return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const params = new URLSearchParams() diff --git a/apps/sim/app/api/tools/jsm/request/route.ts b/apps/sim/app/api/tools/jsm/request/route.ts index 972fa478fe..86f18b5bea 100644 --- a/apps/sim/app/api/tools/jsm/request/route.ts +++ b/apps/sim/app/api/tools/jsm/request/route.ts @@ -1,5 +1,10 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { + validateAlphanumericId, + validateJiraCloudId, + validateJiraIssueKey, +} from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -33,11 +38,26 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const isCreateOperation = serviceDeskId && requestTypeId && summary if (isCreateOperation) { + const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId') + if (!serviceDeskIdValidation.isValid) { + return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) + } + + const requestTypeIdValidation = validateAlphanumericId(requestTypeId, 'requestTypeId') + if (!requestTypeIdValidation.isValid) { + return NextResponse.json({ error: requestTypeIdValidation.error }, { status: 400 }) + } const url = `${baseUrl}/request` logger.info('Creating request at:', url) @@ -95,6 +115,11 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 }) } + const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey') + if (!issueIdOrKeyValidation.isValid) { + return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 }) + } + const url = `${baseUrl}/request/${issueIdOrKey}` logger.info('Fetching request from:', url) diff --git a/apps/sim/app/api/tools/jsm/requests/route.ts b/apps/sim/app/api/tools/jsm/requests/route.ts index fd854257cd..e12ea98fe0 100644 --- a/apps/sim/app/api/tools/jsm/requests/route.ts +++ b/apps/sim/app/api/tools/jsm/requests/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -32,6 +33,19 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + if (serviceDeskId) { + const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId') + if (!serviceDeskIdValidation.isValid) { + return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) + } + } + const baseUrl = getJsmApiBaseUrl(cloudId) const params = new URLSearchParams() diff --git a/apps/sim/app/api/tools/jsm/requesttypes/route.ts b/apps/sim/app/api/tools/jsm/requesttypes/route.ts index e10cbfb9e1..d7e9bdb282 100644 --- a/apps/sim/app/api/tools/jsm/requesttypes/route.ts +++ b/apps/sim/app/api/tools/jsm/requesttypes/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -27,6 +28,17 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId') + if (!serviceDeskIdValidation.isValid) { + return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const params = new URLSearchParams() diff --git a/apps/sim/app/api/tools/jsm/servicedesks/route.ts b/apps/sim/app/api/tools/jsm/servicedesks/route.ts index 4fa01adedd..bab91d5bb7 100644 --- a/apps/sim/app/api/tools/jsm/servicedesks/route.ts +++ b/apps/sim/app/api/tools/jsm/servicedesks/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateJiraCloudId } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -22,6 +23,12 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const params = new URLSearchParams() diff --git a/apps/sim/app/api/tools/jsm/sla/route.ts b/apps/sim/app/api/tools/jsm/sla/route.ts index be02d053ef..ac826afcfc 100644 --- a/apps/sim/app/api/tools/jsm/sla/route.ts +++ b/apps/sim/app/api/tools/jsm/sla/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -27,6 +28,17 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey') + if (!issueIdOrKeyValidation.isValid) { + return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const params = new URLSearchParams() diff --git a/apps/sim/app/api/tools/jsm/transition/route.ts b/apps/sim/app/api/tools/jsm/transition/route.ts index 1f128450c1..5d17a7ca42 100644 --- a/apps/sim/app/api/tools/jsm/transition/route.ts +++ b/apps/sim/app/api/tools/jsm/transition/route.ts @@ -1,5 +1,10 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { + validateAlphanumericId, + validateJiraCloudId, + validateJiraIssueKey, +} from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -38,6 +43,22 @@ export async function POST(request: Request) { } const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey') + if (!issueIdOrKeyValidation.isValid) { + return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 }) + } + + const transitionIdValidation = validateAlphanumericId(transitionId, 'transitionId') + if (!transitionIdValidation.isValid) { + return NextResponse.json({ error: transitionIdValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const url = `${baseUrl}/request/${issueIdOrKey}/transition` diff --git a/apps/sim/app/api/tools/jsm/transitions/route.ts b/apps/sim/app/api/tools/jsm/transitions/route.ts index dc381bc87d..a9242177bf 100644 --- a/apps/sim/app/api/tools/jsm/transitions/route.ts +++ b/apps/sim/app/api/tools/jsm/transitions/route.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -27,6 +28,17 @@ export async function POST(request: Request) { } const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey') + if (!issueIdOrKeyValidation.isValid) { + return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 }) + } + const baseUrl = getJsmApiBaseUrl(cloudId) const url = `${baseUrl}/request/${issueIdOrKey}/transition`