chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,220 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmApprovalsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateEnum,
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { 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 const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmApprovalsContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
action,
issueIdOrKey,
approvalId,
decision,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!action) {
logger.error('Missing action in 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') {
const params = new URLSearchParams()
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/request/${issueIdOrKey}/approval${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching approvals from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
approvals: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
}
if (action === 'answer') {
if (!approvalId) {
logger.error('Missing approvalId in request')
return NextResponse.json({ error: 'Approval ID is required' }, { 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}`
logger.info('Answering approval:', { issueIdOrKey, approvalId, decision })
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ decision }),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
approvalId,
decision,
id: data.id ?? null,
name: data.name ?? null,
finalDecision: data.finalDecision ?? null,
canAnswerApproval: data.canAnswerApproval ?? null,
approvers: (data.approvers ?? []).map((a: Record<string, unknown>) => {
const approver = a.approver as Record<string, unknown> | undefined
return {
approver: {
accountId: approver?.accountId ?? null,
displayName: approver?.displayName ?? null,
emailAddress: approver?.emailAddress ?? null,
active: approver?.active ?? null,
},
approverDecision: a.approverDecision ?? null,
}
}),
createdDate: data.createdDate ?? null,
completedDate: data.completedDate ?? null,
approval: data,
success: true,
},
})
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
logger.error('Error in approvals operation:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,98 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmObjectTypeAttributesContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsAttributesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmObjectTypeAttributesContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectTypeId,
onlyValueEditable,
query: searchQuery,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const query = new URLSearchParams()
if (onlyValueEditable !== undefined) {
query.append('onlyValueEditable', String(onlyValueEditable))
}
if (searchQuery) query.append('query', searchQuery)
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objecttype/${encodeURIComponent(
objectTypeId
)}/attributes${query.toString() ? `?${query.toString()}` : ''}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error getting attributes', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
const attributes = Array.isArray(data) ? data : (data.values ?? [])
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
attributes,
total: attributes.length,
},
})
} catch (error) {
logger.error('Error getting Assets attributes', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,97 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmListObjectTypesContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsObjectTypesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmListObjectTypesContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
schemaId,
excludeAbstract,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const query = new URLSearchParams()
if (excludeAbstract !== undefined) query.append('excludeAbstract', String(excludeAbstract))
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent(
schemaId
)}/objecttypes${query.toString() ? `?${query.toString()}` : ''}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error listing object types', {
status: response.status,
errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
const objectTypes = Array.isArray(data) ? data : (data.values ?? [])
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
objectTypes,
total: objectTypes.length,
},
})
} catch (error) {
logger.error('Error listing Assets object types', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmCreateObjectContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import {
getAssetsApiBaseUrl,
getJsmHeaders,
mapAssetObject,
resolveAssetsContext,
} from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsCreateObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmCreateObjectContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectTypeId,
attributes,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/create`
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ objectTypeId, attributes }),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error creating object', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
})
} catch (error) {
logger.error('Error creating Assets object', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,81 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmDeleteObjectContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsDeleteObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmDeleteObjectContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectId,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
const response = await fetch(url, { method: 'DELETE', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error deleting object', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), objectId, deleted: true },
})
} catch (error) {
logger.error('Error deleting Assets object', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,88 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmGetObjectContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import {
getAssetsApiBaseUrl,
getJsmHeaders,
mapAssetObject,
resolveAssetsContext,
} from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsGetObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmGetObjectContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectId,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error getting object', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
})
} catch (error) {
logger.error('Error getting Assets object', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,97 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmUpdateObjectContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import {
getAssetsApiBaseUrl,
getJsmHeaders,
mapAssetObject,
resolveAssetsContext,
} from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsUpdateObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmUpdateObjectContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectId,
objectTypeId,
attributes,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
const body: Record<string, unknown> = { attributes }
if (objectTypeId) body.objectTypeId = objectTypeId
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error updating object', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
})
} catch (error) {
logger.error('Error updating Assets object', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,83 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmGetObjectSchemaContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsSchemaAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmGetObjectSchemaContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
schemaId,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent(schemaId)}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error getting schema', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), schema: data ?? null },
})
} catch (error) {
logger.error('Error getting Assets schema', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,97 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmListObjectSchemasContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsSchemasAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmListObjectSchemasContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
startAt,
maxResults,
includeCounts,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const query = new URLSearchParams()
if (startAt !== undefined) query.append('startAt', String(startAt))
if (maxResults !== undefined) query.append('maxResults', String(maxResults))
if (includeCounts !== undefined) query.append('includeCounts', String(includeCounts))
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/list${
query.toString() ? `?${query.toString()}` : ''
}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error listing schemas', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
schemas: data.values ?? [],
total: data.total ?? (data.values?.length || 0),
isLast: data.isLast ?? data.last ?? true,
},
})
} catch (error) {
logger.error('Error listing Assets schemas', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,122 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmSearchObjectsAqlContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import {
getAssetsApiBaseUrl,
getJsmHeaders,
mapAssetObject,
resolveAssetsContext,
} from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsSearchAPI')
/** Coerce a string|number|boolean param into a number, falling back when unset */
function toNumber(value: string | number | undefined, fallback: number): number {
if (value === undefined) return fallback
const parsed = typeof value === 'number' ? value : Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmSearchObjectsAqlContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
qlQuery,
page,
resultsPerPage,
includeAttributes,
objectTypeId,
objectSchemaId,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const includeAttrs =
includeAttributes === undefined ? true : String(includeAttributes) === 'true'
const body: Record<string, unknown> = {
qlQuery,
page: toNumber(page, 1),
resultsPerPage: toNumber(resultsPerPage, 25),
includeAttributes: includeAttrs,
}
if (objectTypeId) body.objectTypeId = objectTypeId
if (objectSchemaId) body.objectSchemaId = objectSchemaId
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/aql`
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error running AQL search', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
objects: Array.isArray(data.objectEntries) ? data.objectEntries.map(mapAssetObject) : [],
total: data.totalFilterCount ?? (data.objectEntries?.length || 0),
pageNumber: data.pageNumber ?? 1,
pageSize: data.pageSize ?? (data.objectEntries?.length || 0),
},
})
} catch (error) {
logger.error('Error running Assets AQL search', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
+134
View File
@@ -0,0 +1,134 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmCommentContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmCommentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmCommentContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: providedCloudId,
issueIdOrKey,
body: commentBody,
isPublic,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!commentBody) {
logger.error('Missing comment body in request')
return NextResponse.json({ error: 'Comment body is required' }, { status: 400 })
}
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`
logger.info('Adding comment to:', url)
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({
body: commentBody,
public: isPublic ?? true,
}),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
commentId: data.id,
body: data.body,
isPublic: data.public,
author: data.author
? {
accountId: data.author.accountId ?? null,
displayName: data.author.displayName ?? null,
emailAddress: data.author.emailAddress ?? null,
}
: null,
createdDate: data.created ?? null,
success: true,
},
})
} catch (error) {
logger.error('Error adding comment:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,126 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmCommentsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmCommentsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmCommentsContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
issueIdOrKey,
isPublic,
internal,
expand,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { 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)
const params = new URLSearchParams()
if (isPublic !== undefined) params.append('public', String(isPublic))
if (internal !== undefined) params.append('internal', String(internal))
if (expand) params.append('expand', expand)
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/request/${issueIdOrKey}/comment${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching comments from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
comments: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error fetching comments:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,186 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmCustomersContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmCustomersAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmCustomersContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
serviceDeskId,
query,
start,
limit,
accountIds,
emails,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!serviceDeskId) {
logger.error('Missing serviceDeskId in request')
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
}
if (emails !== undefined) {
return NextResponse.json(
{
error:
'The `emails` parameter is no longer supported. Use `accountIds` (Atlassian account IDs) instead.',
},
{ 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 serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
if (!serviceDeskIdValidation.isValid) {
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmApiBaseUrl(cloudId)
const splitCsv = (value: unknown): string[] =>
value
? typeof value === 'string'
? value
.split(',')
.map((v: string) => v.trim())
.filter((v: string) => v)
: Array.isArray(value)
? (value as string[])
: []
: []
const parsedAccountIds = splitCsv(accountIds)
if (parsedAccountIds.length > 0) {
const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer`
logger.info('Adding customers to:', url, {
accountIds: parsedAccountIds,
})
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ accountIds: parsedAccountIds }),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
serviceDeskId,
success: true,
},
})
}
const params = new URLSearchParams()
if (query) params.append('query', query)
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching customers from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
customers: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error with customers operation:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,117 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmFormAnswersContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmGetFormAnswersAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmFormAnswersContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { 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 formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/format/answers`
logger.info('Getting form answers:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
answers: data ?? null,
},
})
} catch (error) {
logger.error('Error getting form answers:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,131 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmAttachFormContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAttachFormAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmAttachFormContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
issueIdOrKey,
formTemplateId,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formTemplateId) {
logger.error('Missing formTemplateId in request')
return NextResponse.json({ error: 'Form template ID is required' }, { 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 formTemplateIdValidation = validateJiraCloudId(formTemplateId, 'formTemplateId')
if (!formTemplateIdValidation.isValid) {
return NextResponse.json({ error: formTemplateIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form`
logger.info('Attaching form to issue:', { url, issueIdOrKey, formTemplateId })
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({
formTemplate: { id: formTemplateId },
}),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
id: data.id ?? null,
name: data.name ?? null,
updated: data.updated ?? null,
submitted: data.submitted ?? false,
lock: data.lock ?? false,
internal: data.internal ?? null,
formTemplateId: (data.formTemplate as Record<string, unknown>)?.id ?? null,
},
})
} catch (error) {
logger.error('Error attaching form:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,132 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmCopyFormsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmCopyFormsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmCopyFormsContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
sourceIssueIdOrKey,
targetIssueIdOrKey,
formIds,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!sourceIssueIdOrKey) {
logger.error('Missing sourceIssueIdOrKey in request')
return NextResponse.json({ error: 'Source issue ID or key is required' }, { status: 400 })
}
if (!targetIssueIdOrKey) {
logger.error('Missing targetIssueIdOrKey in request')
return NextResponse.json({ error: 'Target issue ID or key is required' }, { 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 sourceValidation = validateJiraIssueKey(sourceIssueIdOrKey, 'sourceIssueIdOrKey')
if (!sourceValidation.isValid) {
return NextResponse.json({ error: sourceValidation.error }, { status: 400 })
}
const targetValidation = validateJiraIssueKey(targetIssueIdOrKey, 'targetIssueIdOrKey')
if (!targetValidation.isValid) {
return NextResponse.json({ error: targetValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(sourceIssueIdOrKey)}/form/copy/${encodeURIComponent(targetIssueIdOrKey)}`
if (formIds !== undefined && !Array.isArray(formIds)) {
return NextResponse.json({ error: 'formIds must be an array of form UUIDs' }, { status: 400 })
}
const requestBody = Array.isArray(formIds) && formIds.length > 0 ? { ids: formIds } : {}
logger.info('Copying forms:', { url, sourceIssueIdOrKey, targetIssueIdOrKey, formIds })
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(requestBody),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
sourceIssueIdOrKey,
targetIssueIdOrKey,
copiedForms: data.copiedForms ?? [],
errors: data.errors ?? [],
},
})
} catch (error) {
logger.error('Error copying forms:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,117 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmDeleteFormContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmDeleteFormAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmDeleteFormContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { 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 formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
logger.info('Deleting form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'DELETE',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
await response.text()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
deleted: true,
},
})
} catch (error) {
logger.error('Error deleting form:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,118 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmExternaliseFormContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmExternaliseFormAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmExternaliseFormContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { 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 formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/external`
logger.info('Externalising form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const bodyText = await response.text()
const data = bodyText ? JSON.parse(bodyText) : {}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
visibility: data.visibility ?? 'external',
},
})
} catch (error) {
logger.error('Error externalising form:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,119 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmGetFormContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmGetFormAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmGetFormContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { 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 formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
logger.info('Getting form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
design: data.design ?? null,
state: data.state ?? null,
updated: data.updated ?? null,
},
})
} catch (error) {
logger.error('Error getting form:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,118 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmInternaliseFormContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmInternaliseFormAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmInternaliseFormContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { 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 formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/internal`
logger.info('Internalising form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const bodyText = await response.text()
const data = bodyText ? JSON.parse(bodyText) : {}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
visibility: data.visibility ?? 'internal',
},
})
} catch (error) {
logger.error('Error internalising form:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,117 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmIssueFormsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmIssueFormsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmIssueFormsContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { 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 = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form`
logger.info('Fetching issue forms from:', { url, issueIdOrKey })
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
const forms = Array.isArray(data) ? data : (data.values ?? data.forms ?? [])
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
forms: forms.map((form: Record<string, unknown>) => ({
id: form.id ?? null,
name: form.name ?? null,
updated: form.updated ?? null,
submitted: form.submitted ?? false,
lock: form.lock ?? false,
internal: form.internal ?? null,
formTemplateId: (form.formTemplate as Record<string, unknown>)?.id ?? null,
})),
total: forms.length,
},
})
} catch (error) {
logger.error('Error fetching issue forms:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,118 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmReopenFormContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmReopenFormAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmReopenFormContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { 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 formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/reopen`
logger.info('Reopening form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const bodyText = await response.text()
const data = bodyText ? JSON.parse(bodyText) : {}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
status: data.status ?? 'open',
},
})
} catch (error) {
logger.error('Error reopening form:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,131 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmSaveFormAnswersContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmSaveFormAnswersAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmSaveFormAnswersContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
issueIdOrKey,
formId,
answers,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
if (!answers || typeof answers !== 'object' || Array.isArray(answers)) {
logger.error('Missing or invalid answers in request')
return NextResponse.json({ error: 'Answers object is required' }, { 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 formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
logger.info('Saving form answers:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ answers }),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
state: data.state ?? null,
updated: data.updated ?? null,
},
})
} catch (error) {
logger.error('Error saving form answers:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,119 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmProjectFormStructureContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmFormStructureAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmProjectFormStructureContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, projectIdOrKey, formId } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!projectIdOrKey) {
logger.error('Missing projectIdOrKey in request')
return NextResponse.json({ error: 'Project ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { 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 projectIdOrKeyValidation = validateJiraIssueKey(projectIdOrKey, 'projectIdOrKey')
if (!projectIdOrKeyValidation.isValid) {
return NextResponse.json({ error: projectIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form/${encodeURIComponent(formId)}`
logger.info('Fetching form template from:', { url, projectIdOrKey, formId })
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
projectIdOrKey,
formId,
design: data.design ?? null,
updated: data.updated ?? null,
publish: data.publish ?? null,
},
})
} catch (error) {
logger.error('Error fetching form structure:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,118 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmSubmitFormContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmSubmitFormAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmSubmitFormContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { 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 formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/submit`
logger.info('Submitting form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const bodyText = await response.text()
const data = bodyText ? JSON.parse(bodyText) : {}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
status: data.status ?? 'submitted',
},
})
} catch (error) {
logger.error('Error submitting form:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,117 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmProjectFormTemplatesContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmFormTemplatesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmProjectFormTemplatesContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, projectIdOrKey } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!projectIdOrKey) {
logger.error('Missing projectIdOrKey in request')
return NextResponse.json({ error: 'Project ID or key is required' }, { 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 projectIdOrKeyValidation = validateJiraIssueKey(projectIdOrKey, 'projectIdOrKey')
if (!projectIdOrKeyValidation.isValid) {
return NextResponse.json({ error: projectIdOrKeyValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form`
logger.info('Fetching project form templates from:', { url, projectIdOrKey })
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
const templates = Array.isArray(data) ? data : (data.values ?? [])
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
projectIdOrKey,
templates: templates.map((template: Record<string, unknown>) => ({
id: template.id ?? null,
name: template.name ?? null,
updated: template.updated ?? null,
issueCreateIssueTypeIds: template.issueCreateIssueTypeIds ?? [],
issueCreateRequestTypeIds: template.issueCreateRequestTypeIds ?? [],
portalRequestTypeIds: template.portalRequestTypeIds ?? [],
recommendedIssueRequestTypeIds: template.recommendedIssueRequestTypeIds ?? [],
})),
total: templates.length,
},
})
} catch (error) {
logger.error('Error fetching form templates:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,198 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmOrganizationContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateEnum,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { 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 const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmOrganizationContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
action,
name,
serviceDeskId,
organizationId,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!action) {
logger.error('Missing action in 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') {
if (!name) {
logger.error('Missing organization name in request')
return NextResponse.json({ error: 'Organization name is required' }, { status: 400 })
}
const url = `${baseUrl}/organization`
logger.info('Creating organization:', { name })
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ name }),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
organizationId: data.id,
name: data.name,
success: true,
},
})
}
if (action === 'add_to_service_desk') {
if (!serviceDeskId) {
logger.error('Missing serviceDeskId in request')
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
}
if (!organizationId) {
logger.error('Missing organizationId in 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 orgIdNumeric = Number.parseInt(String(organizationId).trim(), 10)
if (!Number.isFinite(orgIdNumeric) || orgIdNumeric <= 0) {
return NextResponse.json(
{ error: 'organizationId must be a positive integer' },
{ status: 400 }
)
}
const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization`
logger.info('Adding organization to service desk:', { serviceDeskId, organizationId })
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ organizationId: orgIdNumeric }),
})
if (response.status === 204 || response.ok) {
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
serviceDeskId,
organizationId,
success: true,
},
})
}
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
logger.error('Error in organization operation:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,119 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmOrganizationsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmOrganizationsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmOrganizationsContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
serviceDeskId,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!serviceDeskId) {
logger.error('Missing serviceDeskId in request')
return NextResponse.json({ error: 'Service Desk ID is required' }, { 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 serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
if (!serviceDeskIdValidation.isValid) {
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmApiBaseUrl(cloudId)
const params = new URLSearchParams()
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching organizations from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
organizations: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error fetching organizations:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,195 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmParticipantsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateEnum,
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmParticipantsAPI')
const VALID_ACTIONS = ['get', 'add'] as const
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmParticipantsContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
action,
issueIdOrKey,
accountIds,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!action) {
logger.error('Missing action in 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') {
const params = new URLSearchParams()
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/request/${issueIdOrKey}/participant${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching participants from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
participants: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
}
if (action === 'add') {
if (!accountIds) {
logger.error('Missing accountIds in request')
return NextResponse.json({ error: 'Account IDs are required' }, { status: 400 })
}
const parsedAccountIds =
typeof accountIds === 'string'
? accountIds
.split(',')
.map((id: string) => id.trim())
.filter((id: string) => id)
: accountIds
const url = `${baseUrl}/request/${issueIdOrKey}/participant`
logger.info('Adding participants to:', url, { accountIds: parsedAccountIds })
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ accountIds: parsedAccountIds }),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
participants: data.values || [],
success: true,
},
})
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
} catch (error) {
logger.error('Error in participants operation:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
+121
View File
@@ -0,0 +1,121 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmQueuesContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmQueuesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmQueuesContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
serviceDeskId,
includeCount,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!serviceDeskId) {
logger.error('Missing serviceDeskId in request')
return NextResponse.json({ error: 'Service Desk ID is required' }, { 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 serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
if (!serviceDeskIdValidation.isValid) {
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmApiBaseUrl(cloudId)
const params = new URLSearchParams()
if (includeCount) params.append('includeCount', includeCount)
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/servicedesk/${serviceDeskId}/queue${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching queues from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
queues: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error fetching queues:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
+271
View File
@@ -0,0 +1,271 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmRequestContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmRequestAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmRequestContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
issueIdOrKey,
serviceDeskId,
requestTypeId,
summary,
description,
raiseOnBehalfOf,
requestFieldValues,
formAnswers,
requestParticipants,
channel,
expand,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { 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)
const isCreateOperation = serviceDeskId && requestTypeId && (summary || formAnswers)
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, serviceDeskId, requestTypeId })
const requestBody: Record<string, unknown> = {
serviceDeskId,
requestTypeId,
}
if (formAnswers && typeof formAnswers === 'object') {
// When form answers are provided, use them as the primary data source.
// Per Atlassian docs, fields linked to form questions must NOT also appear
// in requestFieldValues — doing so causes a 400 error.
requestBody.form = { answers: formAnswers }
// Only include explicit requestFieldValues if the caller provided them
// (they know which fields are safe to include alongside form answers).
if (requestFieldValues && typeof requestFieldValues === 'object') {
requestBody.requestFieldValues = requestFieldValues
}
} else if (summary || description || requestFieldValues) {
const fieldValues =
requestFieldValues && typeof requestFieldValues === 'object'
? {
...(!requestFieldValues.summary && summary ? { summary } : {}),
...(!requestFieldValues.description && description ? { description } : {}),
...requestFieldValues,
}
: {
...(summary && { summary }),
...(description && { description }),
}
requestBody.requestFieldValues = fieldValues
}
if (raiseOnBehalfOf) {
requestBody.raiseOnBehalfOf = raiseOnBehalfOf
}
if (requestParticipants) {
requestBody.requestParticipants = Array.isArray(requestParticipants)
? requestParticipants
: typeof requestParticipants === 'string'
? requestParticipants
.split(',')
.map((id: string) => id.trim())
.filter(Boolean)
: []
}
if (channel) {
requestBody.channel = channel
}
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(requestBody),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueId: data.issueId,
issueKey: data.issueKey,
requestTypeId: data.requestTypeId,
serviceDeskId: data.serviceDeskId,
createdDate: data.createdDate ?? null,
currentStatus: data.currentStatus
? {
status: data.currentStatus.status ?? null,
statusCategory: data.currentStatus.statusCategory ?? null,
statusDate: data.currentStatus.statusDate ?? null,
}
: null,
reporter: data.reporter
? {
accountId: data.reporter.accountId ?? null,
displayName: data.reporter.displayName ?? null,
emailAddress: data.reporter.emailAddress ?? null,
}
: null,
success: true,
url: `https://${domain}/browse/${data.issueKey}`,
},
})
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in 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 params = new URLSearchParams()
if (expand) params.append('expand', expand)
const url = `${baseUrl}/request/${issueIdOrKey}${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching request from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueId: data.issueId ?? null,
issueKey: data.issueKey ?? null,
requestTypeId: data.requestTypeId ?? null,
serviceDeskId: data.serviceDeskId ?? null,
createdDate: data.createdDate ?? null,
currentStatus: data.currentStatus
? {
status: data.currentStatus.status ?? null,
statusCategory: data.currentStatus.statusCategory ?? null,
statusDate: data.currentStatus.statusDate ?? null,
}
: null,
reporter: data.reporter
? {
accountId: data.reporter.accountId ?? null,
displayName: data.reporter.displayName ?? null,
emailAddress: data.reporter.emailAddress ?? null,
active: data.reporter.active ?? true,
}
: null,
requestFieldValues: (data.requestFieldValues ?? []).map((fv: Record<string, unknown>) => ({
fieldId: fv.fieldId ?? null,
label: fv.label ?? null,
value: fv.value ?? null,
})),
url: `https://${domain}/browse/${data.issueKey}`,
request: data,
},
})
} catch (error) {
logger.error('Error with request operation:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,161 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmRequestsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateEnum,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmRequestsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmRequestsContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
serviceDeskId,
requestOwnership,
requestStatus,
requestTypeId,
searchTerm,
expand,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { 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 })
}
if (serviceDeskId) {
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
if (!serviceDeskIdValidation.isValid) {
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
}
}
const VALID_REQUEST_OWNERSHIP = [
'OWNED_REQUESTS',
'PARTICIPATED_REQUESTS',
'APPROVER',
'ALL_REQUESTS',
] as const
const VALID_REQUEST_STATUS = ['OPEN_REQUESTS', 'CLOSED_REQUESTS', 'ALL_REQUESTS'] as const
if (requestOwnership) {
const ownershipValidation = validateEnum(
requestOwnership,
VALID_REQUEST_OWNERSHIP,
'requestOwnership'
)
if (!ownershipValidation.isValid) {
return NextResponse.json({ error: ownershipValidation.error }, { status: 400 })
}
}
if (requestStatus) {
const statusValidation = validateEnum(requestStatus, VALID_REQUEST_STATUS, 'requestStatus')
if (!statusValidation.isValid) {
return NextResponse.json({ error: statusValidation.error }, { status: 400 })
}
}
const baseUrl = getJsmApiBaseUrl(cloudId)
const params = new URLSearchParams()
if (serviceDeskId) params.append('serviceDeskId', serviceDeskId)
if (requestOwnership) {
params.append('requestOwnership', requestOwnership)
}
if (requestStatus) {
params.append('requestStatus', requestStatus)
}
if (requestTypeId) params.append('requestTypeId', requestTypeId)
if (searchTerm) params.append('searchTerm', searchTerm)
if (expand) params.append('expand', expand)
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/request${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching requests from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
requests: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error fetching requests:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,135 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmRequestTypeFieldsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmRequestTypeFieldsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmRequestTypeFieldsContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
serviceDeskId,
requestTypeId,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!serviceDeskId) {
logger.error('Missing serviceDeskId in request')
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
}
if (!requestTypeId) {
logger.error('Missing requestTypeId in request')
return NextResponse.json({ error: 'Request Type ID is required' }, { 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 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 baseUrl = getJsmApiBaseUrl(cloudId)
const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype/${requestTypeId}/field`
logger.info('Fetching request type fields from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
serviceDeskId,
requestTypeId,
canAddRequestParticipants: data.canAddRequestParticipants ?? false,
canRaiseOnBehalfOf: data.canRaiseOnBehalfOf ?? false,
requestTypeFields: (data.requestTypeFields ?? []).map((field: Record<string, unknown>) => ({
fieldId: field.fieldId ?? null,
name: field.name ?? null,
description: field.description ?? null,
required: field.required ?? false,
visible: field.visible ?? true,
validValues: field.validValues ?? [],
presetValues: field.presetValues ?? [],
defaultValues: field.defaultValues ?? [],
jiraSchema: field.jiraSchema ?? null,
})),
},
})
} catch (error) {
logger.error('Error fetching request type fields:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,125 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmRequestTypesContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmRequestTypesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmRequestTypesContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
serviceDeskId,
searchQuery,
groupId,
expand,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!serviceDeskId) {
logger.error('Missing serviceDeskId in request')
return NextResponse.json({ error: 'Service Desk ID is required' }, { 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 serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
if (!serviceDeskIdValidation.isValid) {
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmApiBaseUrl(cloudId)
const params = new URLSearchParams()
if (searchQuery) params.append('searchQuery', searchQuery)
if (groupId) params.append('groupId', groupId)
if (expand) params.append('expand', expand)
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching request types from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
requestTypes: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error fetching request types:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,177 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmRequestTypesSelectorContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
const logger = createLogger('JsmSelectorRequestTypesAPI')
export const dynamic = 'force-dynamic'
const JSM_REQUEST_TYPES_PAGE_SIZE = 100
const MAX_JSM_REQUEST_TYPES_PAGES = 50
interface JsmPagedResponse<T> {
values?: T[]
isLastPage?: boolean
_links?: { next?: string }
}
interface JsmRequestTypeValue {
id: string
name: string
}
/**
* Drains the offset-paginated JSM `/servicedesk/{id}/requesttype` endpoint,
* advancing `start` by the number of rows actually returned until
* `isLastPage === true` (or `_links.next` is absent, or a page comes back
* empty). Advancing by the real row count — not the requested `limit` —
* prevents skipping items if the server returns a short non-final page. Bounded
* by `MAX_JSM_REQUEST_TYPES_PAGES`; emits a `logger.warn` and returns the
* partial set rather than looping unbounded when the cap is hit.
*/
async function fetchAllJsmRequestTypes(
requestTypeUrl: string,
accessToken: string
): Promise<{ values: JsmRequestTypeValue[]; lastResponse: Response }> {
const values: JsmRequestTypeValue[] = []
let start = 0
let lastResponse: Response
for (let page = 0; page < MAX_JSM_REQUEST_TYPES_PAGES; page++) {
const url = `${requestTypeUrl}?start=${start}&limit=${JSM_REQUEST_TYPES_PAGE_SIZE}`
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
return { values, lastResponse: response }
}
const data = (await response.json()) as JsmPagedResponse<JsmRequestTypeValue>
lastResponse = response
const pageValues = data.values ?? []
values.push(...pageValues)
if (data.isLastPage === true || !data._links?.next || pageValues.length === 0) {
return { values, lastResponse }
}
start += pageValues.length
if (page === MAX_JSM_REQUEST_TYPES_PAGES - 1) {
logger.warn('JSM request type list hit pagination cap; list may be incomplete', {
pages: MAX_JSM_REQUEST_TYPES_PAGES,
collected: values.length,
})
}
}
return { values, lastResponse: lastResponse! }
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(jsmRequestTypesSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credential, workflowId, domain, serviceDeskId } = parsed.data.body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!serviceDeskId) {
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
}
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
if (!serviceDeskIdValidation.isValid) {
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
}
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const cloudId = await getJiraCloudId(domain, accessToken)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!)
const requestTypeUrl = `${baseUrl}/servicedesk/${serviceDeskIdValidation.sanitized}/requesttype`
const { values, lastResponse } = await fetchAllJsmRequestTypes(requestTypeUrl, accessToken)
if (!lastResponse.ok) {
const errorText = await lastResponse.text()
logger.error('JSM API error:', {
status: lastResponse.status,
statusText: lastResponse.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(
lastResponse.status,
lastResponse.statusText,
errorText
),
},
{ status: lastResponse.status }
)
}
const requestTypes = values.map((rt) => ({
id: rt.id,
name: rt.name,
}))
return NextResponse.json({ requestTypes })
} catch (error) {
logger.error('Error listing JSM request types:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
})
@@ -0,0 +1,167 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmServiceDesksSelectorContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
const logger = createLogger('JsmSelectorServiceDesksAPI')
export const dynamic = 'force-dynamic'
const JSM_SERVICE_DESKS_PAGE_SIZE = 100
const MAX_JSM_SERVICE_DESKS_PAGES = 50
interface JsmPagedResponse<T> {
values?: T[]
isLastPage?: boolean
_links?: { next?: string }
}
interface JsmServiceDeskValue {
id: string
projectName: string
}
/**
* Drains the offset-paginated JSM `/servicedesk` endpoint, advancing `start` by
* the number of rows actually returned until `isLastPage === true` (or
* `_links.next` is absent, or a page comes back empty). Advancing by the real
* row count — not the requested `limit` — prevents skipping items if the server
* returns a short non-final page. Bounded by `MAX_JSM_SERVICE_DESKS_PAGES`;
* emits a `logger.warn` and returns the partial set rather than looping
* unbounded when the cap is hit.
*/
async function fetchAllJsmServiceDesks(
baseUrl: string,
accessToken: string
): Promise<{ values: JsmServiceDeskValue[]; lastResponse: Response }> {
const values: JsmServiceDeskValue[] = []
let start = 0
let lastResponse: Response
for (let page = 0; page < MAX_JSM_SERVICE_DESKS_PAGES; page++) {
const url = `${baseUrl}/servicedesk?start=${start}&limit=${JSM_SERVICE_DESKS_PAGE_SIZE}`
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
return { values, lastResponse: response }
}
const data = (await response.json()) as JsmPagedResponse<JsmServiceDeskValue>
lastResponse = response
const pageValues = data.values ?? []
values.push(...pageValues)
if (data.isLastPage === true || !data._links?.next || pageValues.length === 0) {
return { values, lastResponse }
}
start += pageValues.length
if (page === MAX_JSM_SERVICE_DESKS_PAGES - 1) {
logger.warn('JSM service desk list hit pagination cap; list may be incomplete', {
pages: MAX_JSM_SERVICE_DESKS_PAGES,
collected: values.length,
})
}
}
return { values, lastResponse: lastResponse! }
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(jsmServiceDesksSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credential, workflowId, domain } = parsed.data.body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const cloudId = await getJiraCloudId(domain, accessToken)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!)
const { values, lastResponse } = await fetchAllJsmServiceDesks(baseUrl, accessToken)
if (!lastResponse.ok) {
const errorText = await lastResponse.text()
logger.error('JSM API error:', {
status: lastResponse.status,
statusText: lastResponse.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(
lastResponse.status,
lastResponse.statusText,
errorText
),
},
{ status: lastResponse.status }
)
}
const serviceDesks = values.map((sd) => ({
id: sd.id,
name: sd.projectName,
}))
return NextResponse.json({ serviceDesks })
} catch (error) {
logger.error('Error listing JSM service desks:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
})
@@ -0,0 +1,103 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmServiceDesksContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmServiceDesksAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmServiceDesksContract, request, {})
if (!parsed.success) return parsed.response
const { domain, accessToken, cloudId: cloudIdParam, expand, start, limit } = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { 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)
const params = new URLSearchParams()
if (expand) params.append('expand', expand)
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/servicedesk${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching service desks from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
serviceDesks: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error fetching service desks:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
+120
View File
@@ -0,0 +1,120 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmSlaContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmSlaAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmSlaContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
issueIdOrKey,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { 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)
const params = new URLSearchParams()
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/request/${issueIdOrKey}/sla${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching SLA info from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
slas: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error fetching SLA info:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,138 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmTransitionContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmTransitionAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmTransitionContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: providedCloudId,
issueIdOrKey,
transitionId,
comment,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!transitionId) {
logger.error('Missing transitionId in request')
return NextResponse.json({ error: 'Transition ID is required' }, { status: 400 })
}
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`
logger.info('Transitioning request at:', url)
const body: Record<string, unknown> = {
id: transitionId,
}
if (comment) {
body.additionalComment = {
body: comment,
}
}
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
transitionId,
success: true,
},
})
} catch (error) {
logger.error('Error transitioning request:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,120 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmTransitionsContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmTransitionsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmTransitionsContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
issueIdOrKey,
start,
limit,
} = parsed.data.body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { 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)
const params = new URLSearchParams()
if (start) params.append('start', start)
if (limit) params.append('limit', limit)
const url = `${baseUrl}/request/${issueIdOrKey}/transition${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching transitions from:', url)
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
transitions: data.values || [],
total: data.size || 0,
isLastPage: data.isLastPage ?? true,
},
})
} catch (error) {
logger.error('Error fetching transitions:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})