import { secretHeaders } from 'kody:runtime'
import {
assertSafeHref,
nextStepFor,
resolvePageHref,
TwilioApiError,
type JsonRecord,
} from './helpers.ts'
import {
API_ACCOUNT_PREFIX,
API_ORIGIN,
asRecord,
compactForm,
resolveAccountSidSecretName,
resolveAuthTokenSecretName,
secretPlaceholder,
type TwilioAuthOptions,
} from './validation.ts'
export {
API_HOST,
API_ORIGIN,
DEFAULT_ACCOUNT_SID_SECRET,
DEFAULT_AUTH_TOKEN_SECRET,
accountSidSetupUrl,
authTokenSetupUrl,
isDryRun,
resolveAccountSidSecretName,
resolveAuthTokenSecretName,
} from './validation.ts'
export {
TwilioApiError,
mutationPreview,
nextStepFor,
resolvePageHref,
sanitizeAccount,
} from './helpers.ts'
export function accountResourcePath(accountSidSecret: string, suffix: string): string {
const path = API_ACCOUNT_PREFIX + secretPlaceholder(accountSidSecret) + suffix
if (!path.startsWith(API_ACCOUNT_PREFIX)) {
throw new Error('Twilio account paths must stay under /2010-04-01/Accounts/.')
}
return path
}
export type TwilioRequestInput = TwilioAuthOptions & {
/** Path after /2010-04-01/Accounts/{AccountSid}, e.g. `.json` or `/Messages.json`. */
resource?: string
/** Absolute or origin-relative Twilio Accounts API URL (for next_page_uri). */
href?: string
method?: 'GET' | 'POST'
query?: Record<string, unknown>
body?: Record<string, unknown>
}
export async function twilioRequest(
input: TwilioRequestInput,
): Promise<JsonRecord> {
const accountSidSecret = resolveAccountSidSecretName(input)
const authTokenSecret = resolveAuthTokenSecretName(input)
const method = input.method ?? 'GET'
let href: string
if (input.href) {
href = resolvePageHref(input.href)
} else {
const resource = input.resource ?? '.json'
if (!resource.startsWith('.') && !resource.startsWith('/')) {
throw new Error('resource must start with "." or "/".')
}
href = API_ORIGIN + accountResourcePath(accountSidSecret, resource)
}
const query = compactForm(input.query ?? {})
if (Object.keys(query).length > 0) {
const params = new URLSearchParams(query)
href += (href.includes('?') ? '&' : '?') + params.toString()
}
assertSafeHref(href)
const form = method === 'POST' ? compactForm(input.body ?? {}) : {}
const response = await fetch(href, {
method,
headers: {
Accept: 'application/json',
Authorization: secretHeaders.basic({
usernameSecret: accountSidSecret,
passwordSecret: authTokenSecret,
scope: 'user',
}),
...(method === 'POST'
? { 'Content-Type': 'application/x-www-form-urlencoded' }
: {}),
},
body: method === 'POST' ? new URLSearchParams(form) : undefined,
})
const text = await response.text()
let parsed: JsonRecord = {}
try {
parsed = text ? (JSON.parse(text) as JsonRecord) : {}
} catch {
parsed = { message: text.slice(0, 300) }
}
if (!response.ok) {
const message =
typeof parsed.message === 'string' ? parsed.message : text.slice(0, 300)
throw new TwilioApiError(
response.status,
parsed,
nextStepFor(response.status, message, accountSidSecret, authTokenSecret),
)
}
return asRecord(parsed)
}