import { resolveZendeskAuth, zendeskFetch, type ResolvedZendeskAuth } from './auth.ts'
import { extractPageInfo } from './models.ts'
import type {
DryRunResult,
JsonRecord,
MutationInput,
ZendeskAuthInput,
ZendeskPageInfo,
} from './types.ts'
import { requireString } from './types.ts'
import {
inferOperationFromPath,
isMutatingMethod,
isSearchPath,
mutationPreview,
nextStepForOperation,
normalizeZendeskPath,
permissionForOperation,
zendeskUrl,
type ZendeskOperation,
} from './setup.ts'
export class ZendeskApiError extends Error {
readonly status: number
readonly operation: ZendeskOperation
readonly details: unknown
readonly permission: string
constructor(
message: string,
options: {
status: number
operation: ZendeskOperation
details: unknown
permission: string
},
) {
super(message)
this.name = 'ZendeskApiError'
this.status = options.status
this.operation = options.operation
this.details = options.details
this.permission = options.permission
}
}
export type ZendeskRequestInput = ZendeskAuthInput &
MutationInput & {
path: string
method?: string
query?: JsonRecord
body?: JsonRecord
operation?: ZendeskOperation
}
export type ZendeskResponse<T> = {
data: T
pageInfo: ZendeskPageInfo
status: number
}
function zendeskErrorMessage(body: unknown): string | null {
if (typeof body === 'string' && body.trim().length > 0) {
return body.trim().slice(0, 400)
}
if (!body || typeof body !== 'object') return null
const record = body as {
error?: unknown
description?: unknown
details?: unknown
message?: unknown
}
if (typeof record.description === 'string' && record.description.length > 0) {
return record.description
}
if (typeof record.error === 'string' && record.error.length > 0) return record.error
if (typeof record.message === 'string' && record.message.length > 0) return record.message
if (record.details && typeof record.details === 'object') {
const first = Object.values(record.details as JsonRecord)[0]
if (Array.isArray(first) && first[0] && typeof first[0] === 'object') {
const description = (first[0] as { description?: unknown }).description
if (typeof description === 'string' && description.length > 0) return description
}
}
return null
}
function looksLikeInsufficientPermission(status: number, body: unknown): boolean {
if (status === 401 || status === 403) return true
const message = (zendeskErrorMessage(body) ?? '').toLowerCase()
return (
message.includes('unauthorized') ||
message.includes('forbidden') ||
message.includes('not authorized') ||
message.includes('permission') ||
message.includes('invalid access token') ||
message.includes('scope')
)
}
export async function zendeskRequest<T = unknown>(
input: ZendeskRequestInput,
): Promise<ZendeskResponse<T> | DryRunResult> {
const method = (input.method ?? 'GET').toUpperCase()
const path = normalizeZendeskPath(requireString(input.path, 'path'), input.subdomain)
const operation = inferOperationFromPath(method, path, input.operation)
const mutating = isMutatingMethod(method) && !isSearchPath(path)
if (mutating) {
const authForUrl = await resolveZendeskAuth(input).catch(() => null)
const subdomain = authForUrl?.subdomain ?? input.subdomain
const url = subdomain
? zendeskUrl(path, input.query, subdomain)
: `https://your-subdomain.zendesk.com/api/v2${path}`
const preview = mutationPreview(input, {
method,
path,
url,
body: input.body,
})
if (preview) return preview
}
const auth = await resolveZendeskAuth(input)
return zendeskRequestWithAuth<T>(auth, {
method,
path,
query: input.query,
body: input.body,
operation,
})
}
export async function zendeskRequestWithAuth<T = unknown>(
auth: ResolvedZendeskAuth,
input: {
method: string
path: string
query?: JsonRecord
body?: JsonRecord
operation: ZendeskOperation
},
): Promise<ZendeskResponse<T>> {
const url = zendeskUrl(input.path, input.query, auth.subdomain)
const headers = new Headers({
Accept: 'application/json',
'User-Agent': 'kody-zendesk/1.0',
})
const init: RequestInit = { method: input.method, headers }
if (input.body !== undefined) {
headers.set('Content-Type', 'application/json')
init.body = JSON.stringify(input.body)
}
const response = await zendeskFetch(auth, url, init)
const text = await response.text()
let parsed: unknown = text
try {
parsed = text ? JSON.parse(text) : null
} catch {
parsed = text
}
if (!response.ok || looksLikeInsufficientPermission(response.status, parsed)) {
const permission = permissionForOperation(input.operation)
const firstMessage =
zendeskErrorMessage(parsed) ||
(typeof parsed === 'string' ? parsed.slice(0, 400) : response.statusText)
throw new ZendeskApiError(
[
`Zendesk ${input.operation} failed (${response.status}): ${firstMessage}.`,
`This call needs the "${permission}" scope (or read/write).`,
nextStepForOperation(input.operation, {
authMode: auth.mode,
integrationName: auth.integrationName,
secretName: auth.secretName,
usernameSecretName: auth.usernameSecretName,
subdomain: auth.subdomain,
email: auth.email,
}),
].join(' '),
{
status: response.status,
operation: input.operation,
details: parsed,
permission,
},
)
}
return {
data: parsed as T,
pageInfo: extractPageInfo(parsed),
status: response.status,
}
}
export { inferOperationFromPath, isMutatingMethod, isSearchPath }