import { intercomFetch, resolveIntercomAuth, type ResolvedIntercomAuth } from './auth.ts'
import { extractPageInfo } from './models.ts'
import type {
DryRunResult,
IntercomAuthInput,
IntercomOperation,
IntercomPageInfo,
JsonRecord,
MutationInput,
} from './types.ts'
import { requireString } from './types.ts'
import {
INTERCOM_API_VERSION,
inferOperationFromPath,
intercomUrl,
isMutatingMethod,
isSearchPath,
mutationPreview,
nextStepForOperation,
normalizeIntercomPath,
permissionForOperation,
} from './setup.ts'
export class IntercomApiError extends Error {
readonly status: number
readonly operation: IntercomOperation
readonly details: unknown
readonly permission: string
constructor(
message: string,
options: {
status: number
operation: IntercomOperation
details: unknown
permission: string
},
) {
super(message)
this.name = 'IntercomApiError'
this.status = options.status
this.operation = options.operation
this.details = options.details
this.permission = options.permission
}
}
export type IntercomRequestInput = IntercomAuthInput &
MutationInput & {
path: string
method?: string
query?: JsonRecord
body?: JsonRecord
operation?: IntercomOperation
}
export type IntercomResponse<T> = {
data: T
pageInfo: IntercomPageInfo
status: number
}
function intercomErrorMessage(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 { errors?: unknown; message?: unknown; type?: unknown }
if (Array.isArray(record.errors) && record.errors[0]) {
const first = record.errors[0] as { message?: unknown; code?: unknown }
if (typeof first.message === 'string' && first.message.length > 0) return first.message
if (typeof first.code === 'string' && first.code.length > 0) return first.code
}
if (typeof record.message === 'string' && record.message.length > 0) return record.message
return null
}
function looksLikeInsufficientPermission(status: number, body: unknown): boolean {
if (status === 401 || status === 403) return true
const message = (intercomErrorMessage(body) ?? '').toLowerCase()
return (
message.includes('unauthorized') ||
message.includes('forbidden') ||
message.includes('not authorized') ||
message.includes('permission') ||
message.includes('access token') ||
message.includes('scope')
)
}
export async function intercomRequest<T = unknown>(
input: IntercomRequestInput,
): Promise<IntercomResponse<T> | DryRunResult> {
const method = (input.method ?? 'GET').toUpperCase()
const path = normalizeIntercomPath(requireString(input.path, 'path'))
const operation = inferOperationFromPath(method, path, input.operation)
const mutating = isMutatingMethod(method) && !isSearchPath(path)
const url = intercomUrl(path, input.query, input)
if (mutating) {
const preview = mutationPreview(input, {
method,
path,
url,
body: input.body,
})
if (preview) return preview
}
const auth = await resolveIntercomAuth(input)
return intercomRequestWithAuth<T>(auth, {
method,
path,
query: input.query,
body: input.body,
operation,
apiVersion: input.apiVersion,
})
}
export async function intercomRequestWithAuth<T = unknown>(
auth: ResolvedIntercomAuth,
input: {
method: string
path: string
query?: JsonRecord
body?: JsonRecord
operation: IntercomOperation
apiVersion?: string
},
): Promise<IntercomResponse<T>> {
const url = intercomUrl(input.path, input.query, { apiBaseUrl: auth.apiBaseUrl })
const headers = new Headers({
Accept: 'application/json',
'Intercom-Version': input.apiVersion ?? INTERCOM_API_VERSION,
'User-Agent': 'kody-intercom/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 intercomFetch(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 =
intercomErrorMessage(parsed) ||
(typeof parsed === 'string' ? parsed.slice(0, 400) : response.statusText)
throw new IntercomApiError(
[
`Intercom ${input.operation} failed (${response.status}): ${firstMessage}.`,
`This call needs the "${permission}" permission.`,
nextStepForOperation(input.operation, {
authMode: auth.mode,
integrationName: auth.integrationName,
secretName: auth.secretName,
}),
].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 }