From cc065d4d37d2c1eb1c9b1dd0438517c83c4f0d3a Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Wed, 26 Aug 2026 12:06:59 -0700 Subject: [PATCH] fix(jsm): resolve selector contexts server-side --- .../tools/jsm/selector-requesttypes/route.ts | 103 +++++----- .../tools/jsm/selector-servicedesks/route.ts | 103 +++++----- .../jsm/server-resolved-selectors.test.ts | 176 ++++++++++++++++++ .../selectors/providers/jsm/selectors.ts | 12 +- .../jsm/server-resolved-context.test.ts | 67 +++++++ apps/sim/lib/api/contracts/selectors/jsm.ts | 4 +- 6 files changed, 366 insertions(+), 99 deletions(-) create mode 100644 apps/sim/app/api/tools/jsm/server-resolved-selectors.test.ts create mode 100644 apps/sim/hooks/selectors/providers/jsm/server-resolved-context.test.ts diff --git a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts index 705dc2be2e0..193140016bd 100644 --- a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts @@ -2,12 +2,20 @@ 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 '@/lib/oauth/credential-service' -import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { resolveAtlassianSelectorCredential } from '@/lib/selectors/application/atlassian-credential' +import { + resolveSelectorProviderValue, + SELECTOR_ATLASSIAN_DISCOVERY_OPTIONS, + selectorProviderFailure, +} from '@/lib/selectors/server/provider-errors' +import { + authenticateSelectorRequest, + resolveAuthorizedSelectorContext, +} from '@/lib/selectors/server/resolve-authorized-context' +import { getJiraCloudId } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' const logger = createLogger('JsmSelectorRequestTypesAPI') @@ -83,20 +91,20 @@ async function fetchAllJsmRequestTypes( export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { + const authentication = await authenticateSelectorRequest(request) + if (!authentication.ok) { + return NextResponse.json({ error: authentication.error }, { status: authentication.status }) + } const parsed = await parseRequest(jsmRequestTypesSelectorContract, request, {}) if (!parsed.success) return parsed.response - const { credential, workflowId, domain, serviceDeskId } = parsed.data.body + const { credential, workflowId, domain: domainReference, 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 }) } @@ -106,31 +114,50 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) } - const authz = await authorizeCredentialUse(request, { - credentialId: credential, + const resolution = await resolveAuthorizedSelectorContext(authentication.principal, { workflowId, + credentialId: credential, + context: { domain: domainReference }, }) - if (!authz.ok || !authz.credentialOwnerUserId) { - return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) + if (!resolution.ok) { + return NextResponse.json({ error: resolution.error }, { status: resolution.status }) } - - const accessToken = await refreshAccessTokenIfNeeded( - credential, - authz.credentialOwnerUserId, - requestId - ) - if (!accessToken) { - logger.error('Failed to get access token', { - credentialId: credential, - userId: authz.credentialOwnerUserId, - }) + const ownerUserId = resolution.credentialAccess?.credentialOwnerUserId + if (!ownerUserId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + const bundle = await resolveAtlassianSelectorCredential({ + credentialId: credential, + credentialOwnerUserId: ownerUserId, + requestId, + serviceId: 'jira', + }) + if (!bundle) { + logger.error('Failed to get JSM selector access token') return NextResponse.json( { error: 'Could not retrieve access token', authRequired: true }, { status: 401 } ) } - const cloudId = await getJiraCloudId(domain, accessToken) + const domain = resolution.context.domain as string + const accessToken = bundle.accessToken + const cloudIdResolution = await resolveSelectorProviderValue( + 'Jira Service Management', + async () => + bundle.cloudId + ? bundle.cloudId + : getJiraCloudId(domain, accessToken, SELECTOR_ATLASSIAN_DISCOVERY_OPTIONS) + ) + if (!cloudIdResolution.ok) { + logger.warn('JSM selector discovery failed', { + status: cloudIdResolution.upstreamStatus ?? 'unknown', + }) + return NextResponse.json(cloudIdResolution.failure, { + status: cloudIdResolution.failure.status, + }) + } + const cloudId = cloudIdResolution.value const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') if (!cloudIdValidation.isValid) { @@ -143,22 +170,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { 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 } - ) + logger.warn('JSM selector request-type request failed', { status: lastResponse.status }) + const failure = selectorProviderFailure('Jira Service Management', lastResponse.status) + return NextResponse.json(failure, { status: failure.status }) } const requestTypes = values.map((rt) => ({ @@ -167,11 +181,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { })) 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 } - ) + } catch { + logger.error('Error listing JSM request types') + return NextResponse.json({ error: 'Failed to retrieve JSM request types' }, { status: 500 }) } }) diff --git a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts index 9dc55ea0a83..ca958821040 100644 --- a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts @@ -2,12 +2,20 @@ 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 '@/lib/oauth/credential-service' -import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { resolveAtlassianSelectorCredential } from '@/lib/selectors/application/atlassian-credential' +import { + resolveSelectorProviderValue, + SELECTOR_ATLASSIAN_DISCOVERY_OPTIONS, + selectorProviderFailure, +} from '@/lib/selectors/server/provider-errors' +import { + authenticateSelectorRequest, + resolveAuthorizedSelectorContext, +} from '@/lib/selectors/server/resolve-authorized-context' +import { getJiraCloudId } from '@/tools/jira/utils' import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' const logger = createLogger('JsmSelectorServiceDesksAPI') @@ -83,45 +91,64 @@ async function fetchAllJsmServiceDesks( export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { + const authentication = await authenticateSelectorRequest(request) + if (!authentication.ok) { + return NextResponse.json({ error: authentication.error }, { status: authentication.status }) + } const parsed = await parseRequest(jsmServiceDesksSelectorContract, request, {}) if (!parsed.success) return parsed.response - const { credential, workflowId, domain } = parsed.data.body + const { credential, workflowId, domain: domainReference } = 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, + const resolution = await resolveAuthorizedSelectorContext(authentication.principal, { workflowId, + credentialId: credential, + context: { domain: domainReference }, }) - if (!authz.ok || !authz.credentialOwnerUserId) { - return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) + if (!resolution.ok) { + return NextResponse.json({ error: resolution.error }, { status: resolution.status }) } - - const accessToken = await refreshAccessTokenIfNeeded( - credential, - authz.credentialOwnerUserId, - requestId - ) - if (!accessToken) { - logger.error('Failed to get access token', { - credentialId: credential, - userId: authz.credentialOwnerUserId, - }) + const ownerUserId = resolution.credentialAccess?.credentialOwnerUserId + if (!ownerUserId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + const bundle = await resolveAtlassianSelectorCredential({ + credentialId: credential, + credentialOwnerUserId: ownerUserId, + requestId, + serviceId: 'jira', + }) + if (!bundle) { + logger.error('Failed to get JSM selector access token') return NextResponse.json( { error: 'Could not retrieve access token', authRequired: true }, { status: 401 } ) } - const cloudId = await getJiraCloudId(domain, accessToken) + const domain = resolution.context.domain as string + const accessToken = bundle.accessToken + const cloudIdResolution = await resolveSelectorProviderValue( + 'Jira Service Management', + async () => + bundle.cloudId + ? bundle.cloudId + : getJiraCloudId(domain, accessToken, SELECTOR_ATLASSIAN_DISCOVERY_OPTIONS) + ) + if (!cloudIdResolution.ok) { + logger.warn('JSM selector discovery failed', { + status: cloudIdResolution.upstreamStatus ?? 'unknown', + }) + return NextResponse.json(cloudIdResolution.failure, { + status: cloudIdResolution.failure.status, + }) + } + const cloudId = cloudIdResolution.value const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') if (!cloudIdValidation.isValid) { @@ -133,22 +160,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { 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 } - ) + logger.warn('JSM selector service-desk request failed', { status: lastResponse.status }) + const failure = selectorProviderFailure('Jira Service Management', lastResponse.status) + return NextResponse.json(failure, { status: failure.status }) } const serviceDesks = values.map((sd) => ({ @@ -157,11 +171,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { })) 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 } - ) + } catch { + logger.error('Error listing JSM service desks') + return NextResponse.json({ error: 'Failed to retrieve JSM service desks' }, { status: 500 }) } }) diff --git a/apps/sim/app/api/tools/jsm/server-resolved-selectors.test.ts b/apps/sim/app/api/tools/jsm/server-resolved-selectors.test.ts new file mode 100644 index 00000000000..82f7a2793d9 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/server-resolved-selectors.test.ts @@ -0,0 +1,176 @@ +/** + * @vitest-environment node + */ +import { createMockRequest } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + authenticate: vi.fn(), + resolveContext: vi.fn(), + resolveAtlassianCredential: vi.fn(), +})) + +vi.mock('@/lib/selectors/server/resolve-authorized-context', () => ({ + authenticateSelectorRequest: mocks.authenticate, + resolveAuthorizedSelectorContext: mocks.resolveContext, +})) + +vi.mock('@/lib/selectors/application/atlassian-credential', () => ({ + resolveAtlassianSelectorCredential: mocks.resolveAtlassianCredential, +})) + +import { POST as listRequestTypes } from '@/app/api/tools/jsm/selector-requesttypes/route' +import { POST as listServiceDesks } from '@/app/api/tools/jsm/selector-servicedesks/route' + +function request(path: string, body: unknown) { + return createMockRequest('POST', body, {}, `http://localhost:3000${path}`) +} + +describe('server-resolved JSM selectors', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.authenticate.mockResolvedValue({ + ok: true, + principal: { type: 'session', userId: 'viewer-1' }, + }) + mocks.resolveContext.mockImplementation( + async (_principal: unknown, input: { context: Record }) => ({ + ok: true, + context: { ...input.context, domain: 'resolved.example.com' }, + requesterUserId: 'viewer-1', + workspaceId: 'workspace-1', + credentialAccess: { credentialOwnerUserId: 'owner-1' }, + }) + ) + mocks.resolveAtlassianCredential.mockResolvedValue({ + accessToken: 'atlassian-token', + cloudId: 'cloud-id-1', + }) + }) + + it('authenticates before parsing malformed requests', async () => { + mocks.authenticate.mockResolvedValue({ ok: false, status: 401, error: 'Unauthorized' }) + + const response = await listServiceDesks(request('/api/tools/jsm/selector-servicedesks', {})) + + expect(response.status).toBe(401) + expect(mocks.resolveContext).not.toHaveBeenCalled() + }) + + it('short-circuits inaccessible references before credential or provider access', async () => { + mocks.resolveContext.mockResolvedValue({ + ok: false, + status: 400, + error: 'Unable to resolve selector configuration', + }) + const providerFetch = vi.fn() + vi.stubGlobal('fetch', providerFetch) + + const response = await listServiceDesks( + request('/api/tools/jsm/selector-servicedesks', { + credential: 'credential-1', + workflowId: 'workflow-1', + domain: '{{INACCESSIBLE_DOMAIN}}', + }) + ) + + expect(response.status).toBe(400) + expect(await response.json()).toEqual({ + error: 'Unable to resolve selector configuration', + }) + expect(mocks.resolveAtlassianCredential).not.toHaveBeenCalled() + expect(providerFetch).not.toHaveBeenCalled() + }) + + it('preserves the raw domain reference and drains service-desk pagination', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce( + Response.json({ + values: [{ id: '12', projectName: 'Support' }], + isLastPage: false, + _links: { next: 'next' }, + }) + ) + .mockResolvedValueOnce( + Response.json({ values: [{ id: '13', projectName: 'IT' }], isLastPage: true }) + ) + ) + + const response = await listServiceDesks( + request('/api/tools/jsm/selector-servicedesks', { + credential: 'credential-1', + workflowId: 'workflow-1', + domain: '{{SHARED_DOMAIN}}', + }) + ) + + expect(await response.json()).toEqual({ + serviceDesks: [ + { id: '12', name: 'Support' }, + { id: '13', name: 'IT' }, + ], + }) + expect(mocks.resolveContext).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ context: { domain: '{{SHARED_DOMAIN}}' } }) + ) + expect(fetch).toHaveBeenNthCalledWith( + 2, + expect.stringContaining('start=1&limit=100'), + expect.anything() + ) + }) + + it('supports workflowless credential-backed request-type selectors', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + Response.json({ values: [{ id: '21', name: 'Incident' }], isLastPage: true }) + ) + ) + + const response = await listRequestTypes( + request('/api/tools/jsm/selector-requesttypes', { + credential: 'credential-1', + domain: 'tenant.atlassian.net', + serviceDeskId: '12', + }) + ) + + expect(await response.json()).toEqual({ requestTypes: [{ id: '21', name: 'Incident' }] }) + expect(mocks.resolveContext).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ workflowId: undefined, credentialId: 'credential-1' }) + ) + expect(mocks.resolveAtlassianCredential).toHaveBeenCalledWith( + expect.objectContaining({ serviceId: 'jira' }) + ) + }) + + it('maps provider failures without reading or returning provider bodies', async () => { + const providerText = vi.fn().mockResolvedValue('provider-secret-marker') + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ ok: false, status: 418, text: providerText }) + ) + + const response = await listServiceDesks( + request('/api/tools/jsm/selector-servicedesks', { + credential: 'credential-1', + workflowId: 'workflow-1', + domain: 'tenant.atlassian.net', + }) + ) + expect(response.status).toBe(502) + expect(await response.json()).toEqual({ + error: 'Jira Service Management selector discovery failed.', + status: 502, + }) + expect(providerText).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/hooks/selectors/providers/jsm/selectors.ts b/apps/sim/hooks/selectors/providers/jsm/selectors.ts index 5af8d63c03c..4c45896fb4e 100644 --- a/apps/sim/hooks/selectors/providers/jsm/selectors.ts +++ b/apps/sim/hooks/selectors/providers/jsm/selectors.ts @@ -7,12 +7,12 @@ export const jsmSelectors = { 'jsm.serviceDesks': { key: 'jsm.serviceDesks', contracts: [selectorContracts.jsmServiceDesksSelectorContract], + serverResolvedContextFields: ['domain'], staleTime: SELECTOR_STALE, getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'jsm.serviceDesks', context.oauthCredential ?? 'none', - context.domain ?? 'none', ], enabled: ({ context }) => Boolean(context.oauthCredential && context.domain), fetchList: async ({ context, signal }: SelectorQueryArgs) => { @@ -21,7 +21,7 @@ export const jsmSelectors = { const data = await requestJson(selectorContracts.jsmServiceDesksSelectorContract, { body: { credential: credentialId, - workflowId: context.workflowId, + ...(context.workflowId ? { workflowId: context.workflowId } : {}), domain, }, signal, @@ -38,7 +38,7 @@ export const jsmSelectors = { const data = await requestJson(selectorContracts.jsmServiceDesksSelectorContract, { body: { credential: credentialId, - workflowId: context.workflowId, + ...(context.workflowId ? { workflowId: context.workflowId } : {}), domain, }, signal, @@ -51,12 +51,12 @@ export const jsmSelectors = { 'jsm.requestTypes': { key: 'jsm.requestTypes', contracts: [selectorContracts.jsmRequestTypesSelectorContract], + serverResolvedContextFields: ['domain'], staleTime: SELECTOR_STALE, getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'jsm.requestTypes', context.oauthCredential ?? 'none', - context.domain ?? 'none', context.serviceDeskId ?? 'none', ], enabled: ({ context }) => @@ -68,7 +68,7 @@ export const jsmSelectors = { const data = await requestJson(selectorContracts.jsmRequestTypesSelectorContract, { body: { credential: credentialId, - workflowId: context.workflowId, + ...(context.workflowId ? { workflowId: context.workflowId } : {}), domain, serviceDeskId: context.serviceDeskId, }, @@ -87,7 +87,7 @@ export const jsmSelectors = { const data = await requestJson(selectorContracts.jsmRequestTypesSelectorContract, { body: { credential: credentialId, - workflowId: context.workflowId, + ...(context.workflowId ? { workflowId: context.workflowId } : {}), domain, serviceDeskId: context.serviceDeskId, }, diff --git a/apps/sim/hooks/selectors/providers/jsm/server-resolved-context.test.ts b/apps/sim/hooks/selectors/providers/jsm/server-resolved-context.test.ts new file mode 100644 index 00000000000..7699757dabe --- /dev/null +++ b/apps/sim/hooks/selectors/providers/jsm/server-resolved-context.test.ts @@ -0,0 +1,67 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ requestJson: vi.fn() })) + +vi.mock('@/lib/api/client/request', () => ({ requestJson: mocks.requestJson })) + +import { jsmSelectors } from '@/hooks/selectors/providers/jsm/selectors' + +describe('JSM server-resolved selector context', () => { + beforeEach(() => vi.clearAllMocks()) + + it('opts both selectors into server-resolved domain handling', () => { + expect(jsmSelectors['jsm.serviceDesks'].serverResolvedContextFields).toEqual(['domain']) + expect(jsmSelectors['jsm.requestTypes'].serverResolvedContextFields).toEqual(['domain']) + }) + + it('keeps literal domains out of JSM base query keys', () => { + const context = { + oauthCredential: 'credential-1', + domain: 'private-tenant.atlassian.net', + serviceDeskId: '12', + } + const keys = [ + jsmSelectors['jsm.serviceDesks'].getQueryKey!({ key: 'jsm.serviceDesks', context }), + jsmSelectors['jsm.requestTypes'].getQueryKey!({ key: 'jsm.requestTypes', context }), + ] + + expect(JSON.stringify(keys)).not.toContain(context.domain) + }) + + it('forwards a raw reference and maps options', async () => { + mocks.requestJson.mockResolvedValue({ serviceDesks: [{ id: '12', name: 'Support' }] }) + + const options = await jsmSelectors['jsm.serviceDesks'].fetchList!({ + key: 'jsm.serviceDesks', + context: { + workflowId: 'workflow-1', + oauthCredential: 'credential-1', + domain: '{{SHARED_DOMAIN}}', + }, + }) + + expect(options).toEqual([{ id: '12', label: 'Support' }]) + expect(mocks.requestJson.mock.calls[0][1].body).toEqual({ + credential: 'credential-1', + workflowId: 'workflow-1', + domain: '{{SHARED_DOMAIN}}', + }) + }) + + it('keeps workflowless knowledge-connector selectors enabled', async () => { + const definition = jsmSelectors['jsm.serviceDesks'] + const context = { + workspaceId: 'workspace-1', + oauthCredential: 'credential-1', + domain: 'tenant.atlassian.net', + } + + expect(definition.enabled?.({ key: 'jsm.serviceDesks', context })).toBe(true) + mocks.requestJson.mockResolvedValue({ serviceDesks: [] }) + await definition.fetchList!({ key: 'jsm.serviceDesks', context }) + expect(mocks.requestJson.mock.calls[0][1].body).not.toHaveProperty('workflowId') + }) +}) diff --git a/apps/sim/lib/api/contracts/selectors/jsm.ts b/apps/sim/lib/api/contracts/selectors/jsm.ts index 7dd2706f3db..5cfb561655f 100644 --- a/apps/sim/lib/api/contracts/selectors/jsm.ts +++ b/apps/sim/lib/api/contracts/selectors/jsm.ts @@ -56,6 +56,8 @@ export const jsmRequestTypesBodySchema = credentialWorkflowDomainBodySchema.exte serviceDeskId: z.string().min(1), }) +export const jsmServiceDesksSelectorBodySchema = credentialWorkflowDomainBodySchema + export const jsmServiceDesksBodySchema = jsmBaseBodySchema.extend({ expand: z.string().optional(), start: jsmPaginationField, @@ -291,7 +293,7 @@ export const defineJsmToolContract = (path: string, bod export const jsmServiceDesksSelectorContract = definePostSelector( '/api/tools/jsm/selector-servicedesks', - credentialWorkflowDomainBodySchema, + jsmServiceDesksSelectorBodySchema, z.object({ serviceDesks: z.array(idNameSchema) }) )