Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/lib/credentials/token-service-accounts/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export interface ProviderResponse {
export function isVendorJsonForbidden(response: ProviderResponse): boolean {
return (
response.status === 403 &&
(response.headers.get('content-type') ?? '').includes('application/json')
(response.headers.get('content-type') ?? '').toLowerCase().includes('application/json')
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ describe('validateHubspotServiceAccount', () => {
'lacks the contacts scope',
() => jsonResponse({ status: 'error', category: 'MISSING_SCOPES' }, 403),
],
[
'lacks the contacts scope, answered with a mixed-case JSON media type',
() =>
new Response(JSON.stringify({ status: 'error', category: 'MISSING_SCOPES' }), {
status: 403,
headers: { 'Content-Type': 'Application/JSON; charset=UTF-8' },
}),
],
])(
'accepts a service key that account-info refuses but that %s',
async (_case, contactsAnswer) => {
Expand Down
20 changes: 20 additions & 0 deletions apps/sim/lib/internal/zoominfo/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ describe('ZoomInfo requests', () => {
expect(mockSecureFetchWithValidation).not.toHaveBeenCalled()
})

it.each([
['a blank', ' ', 'client-blank-token'],
['a non-string', 123, 'client-numeric-token'],
])(
'refuses %s access token in the token response before calling the API',
async (_case, token, clientId) => {
mockSecureFetchWithValidation.mockResolvedValue(
Response.json({ access_token: token, expires_in: 3600 })
)

await expect(
requestZoomInfo({ ...REQUEST, clientId, clientSecret: 'secret' }, 'request-4')
).rejects.toMatchObject({
message: 'ZoomInfo token response missing access_token',
status: 500,
})
expect(mockSecureFetchWithValidation).toHaveBeenCalledTimes(1)
}
)

it('keeps reporting an inline token refusal exactly as before', async () => {
mockSecureFetchWithValidation.mockResolvedValue(
Response.json({ error: 'invalid_client' }, { status: 401 })
Expand Down
9 changes: 3 additions & 6 deletions apps/sim/lib/internal/zoominfo/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createHash } from 'node:crypto'
import { createLogger } from '@sim/logger'
import { isRecordLike } from '@sim/utils/object'
import {
MAX_JSON_API_RESPONSE_BYTES,
secureFetchWithValidation,
Expand Down Expand Up @@ -130,12 +131,8 @@ export async function requestZoomInfoToken(
logger.warn('ZoomInfo token fetch failed', { requestId, status: response.status, error: text })
throw new ZoomInfoTokenError(response.status)
}
const data = (await response.json()) as {
access_token?: string
expires_in?: number
scope?: string
}
if (!data.access_token) {
const data: unknown = await response.json()
if (!isRecordLike(data) || typeof data.access_token !== 'string' || !data.access_token.trim()) {
throw new ZoomInfoOperationError('ZoomInfo token response missing access_token', 500)
}
return {
Expand Down
Loading