import { createAuthenticatedFetch, kody, secretHeaders } from 'kody:runtime'
import { resolveEmail, resolveSubdomain } from './config.ts'
import type { ZendeskAuthInput, ZendeskAuthMode } from './types.ts'
import { requireString } from './types.ts'
import {
DEFAULT_INTEGRATION_NAME,
DEFAULT_TOKEN_SECRET,
DEFAULT_USERNAME_SECRET,
missingCredentialsMessage,
resolveIntegrationName,
resolveSecretName,
resolveUsernameSecretName,
} from './setup.ts'
const authenticatedFetches = new Map<string, typeof fetch>()
export type ResolvedZendeskAuth = {
mode: ZendeskAuthMode
integrationName: string
secretName: string
usernameSecretName: string
subdomain: string
email?: string
authorization: string
}
function secretEntries(
result: unknown,
): Array<{ name?: string; scope?: string }> {
if (
result &&
typeof result === 'object' &&
Array.isArray((result as { secrets?: unknown }).secrets)
) {
return (result as { secrets: Array<{ name?: string; scope?: string }> }).secrets
}
return Array.isArray(result) ? result : []
}
export async function listUserSecretNames(): Promise<Set<string>> {
const listed = await kody.secret_list({ scope: 'user' })
const names = new Set<string>()
for (const entry of secretEntries(listed)) {
if (entry?.name && (entry.scope === 'user' || !entry.scope)) {
names.add(entry.name)
}
}
return names
}
export async function listZendeskIntegrationNames(): Promise<Array<string>> {
const listed = await kody.integration_list({})
const integrations =
listed && typeof listed === 'object' && Array.isArray((listed as { integrations?: unknown }).integrations)
? (listed as { integrations: Array<{ name?: string }> }).integrations
: Array.isArray(listed)
? listed
: []
return integrations
.map((item) => (typeof item?.name === 'string' ? item.name : ''))
.filter((name) => name === 'zendesk' || name.startsWith('zendesk-'))
}
export async function resolveZendeskAuth(
input: ZendeskAuthInput = {},
): Promise<ResolvedZendeskAuth> {
const integrationName = resolveIntegrationName(input)
const secretName = resolveSecretName(input)
const usernameSecretName = resolveUsernameSecretName(input)
const subdomain = await resolveSubdomain(input)
const email = await resolveEmail(input)
const forced = input.auth
if (forced !== undefined && forced !== 'oauth' && forced !== 'apiToken') {
throw new Error('auth must be "oauth" or "apiToken".')
}
const [secrets, integrations] = await Promise.all([
listUserSecretNames(),
listZendeskIntegrationNames(),
])
const hasOauth = integrations.includes(integrationName)
const hasToken = secrets.has(secretName) && secrets.has(usernameSecretName)
let mode: ZendeskAuthMode
if (forced === 'oauth') {
if (!hasOauth) {
throw new Error(
missingCredentialsMessage({
integrationName,
secretName,
usernameSecretName,
subdomain,
email,
}),
)
}
mode = 'oauth'
} else if (forced === 'apiToken') {
if (!hasToken) {
throw new Error(
missingCredentialsMessage({
integrationName,
secretName,
usernameSecretName,
subdomain,
email,
}),
)
}
mode = 'apiToken'
} else if (hasOauth) {
mode = 'oauth'
} else if (hasToken) {
mode = 'apiToken'
} else {
throw new Error(
missingCredentialsMessage({
integrationName,
secretName,
usernameSecretName,
subdomain,
email,
}),
)
}
switch (mode) {
case 'oauth':
return {
mode,
integrationName,
secretName,
usernameSecretName,
subdomain,
email,
authorization: `[managed ${integrationName} OAuth integration]`,
}
case 'apiToken':
return {
mode,
integrationName,
secretName,
usernameSecretName,
subdomain,
email,
authorization: secretHeaders.basic({
usernameSecret: usernameSecretName,
passwordSecret: secretName,
scope: 'user',
}),
}
default: {
const exhaustive: never = mode
throw new Error(`Unsupported Zendesk auth mode: ${String(exhaustive)}`)
}
}
}
async function getOauthFetch(integrationName: string): Promise<typeof fetch> {
let authedFetch = authenticatedFetches.get(integrationName)
if (!authedFetch) {
try {
authedFetch = await createAuthenticatedFetch(integrationName)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
throw new Error(
[
`Zendesk OAuth integration "${integrationName}" is not usable: ${message}`,
missingCredentialsMessage({
integrationName,
secretName: DEFAULT_TOKEN_SECRET,
usernameSecretName: DEFAULT_USERNAME_SECRET,
}),
].join(' '),
)
}
authenticatedFetches.set(integrationName, authedFetch)
}
return authedFetch
}
export async function zendeskFetch(
auth: ResolvedZendeskAuth,
url: string,
init: RequestInit,
): Promise<Response> {
switch (auth.mode) {
case 'oauth': {
const authedFetch = await getOauthFetch(auth.integrationName)
return authedFetch(url, init)
}
case 'apiToken': {
const headers = new Headers(init.headers)
headers.set(
'Authorization',
secretHeaders.basic({
usernameSecret: auth.usernameSecretName,
passwordSecret: auth.secretName,
scope: 'user',
}),
)
return fetch(url, { ...init, headers })
}
default: {
const exhaustive: never = auth.mode
throw new Error(`Unsupported Zendesk auth mode: ${String(exhaustive)}`)
}
}
}
export function requireAuthMode(value: unknown): ZendeskAuthMode | undefined {
if (value === undefined) return undefined
const mode = requireString(value, 'auth')
if (mode !== 'oauth' && mode !== 'apiToken') {
throw new Error('auth must be "oauth" or "apiToken".')
}
return mode
}
export function parseAuth(input: Record<string, unknown>): ZendeskAuthInput {
return {
integrationName: optionalStringSafe(input.integrationName, 'integrationName'),
integration: optionalStringSafe(input.integration, 'integration'),
account: optionalStringSafe(input.account, 'account'),
secretName: optionalStringSafe(input.secretName, 'secretName'),
usernameSecretName: optionalStringSafe(input.usernameSecretName, 'usernameSecretName'),
auth: requireAuthMode(input.auth),
subdomain: optionalStringSafe(input.subdomain, 'subdomain'),
email: optionalStringSafe(input.email, 'email'),
}
}
function optionalStringSafe(value: unknown, label: string): string | undefined {
if (value === undefined || value === null) return undefined
return requireString(value, label)
}
export { DEFAULT_INTEGRATION_NAME, DEFAULT_TOKEN_SECRET, DEFAULT_USERNAME_SECRET }