import {
API_ACCOUNT_PREFIX,
API_HOST,
API_ORIGIN,
accountSidSetupUrl,
authTokenSetupUrl,
compactForm,
isDryRun,
resolveAccountSidSecretName,
resolveAuthTokenSecretName,
type TwilioAuthOptions,
} from './validation.ts'
export type JsonRecord = Record<string, unknown>
export class TwilioApiError extends Error {
readonly status: number
readonly code: number | null
readonly twilioMessage: string
readonly moreInfo: string | null
readonly nextStep: string
readonly details: JsonRecord
constructor(status: number, details: JsonRecord, nextStep: string) {
const twilioMessage =
typeof details.message === 'string' && details.message.length > 0
? details.message
: 'Twilio API request failed'
const code = typeof details.code === 'number' ? details.code : null
super(
'Twilio API failed (' +
status +
(code != null ? '/' + code : '') +
'): ' +
twilioMessage +
'. Next step: ' +
nextStep,
)
this.name = 'TwilioApiError'
this.status = status
this.code = code
this.twilioMessage = twilioMessage
this.moreInfo =
typeof details.more_info === 'string' ? details.more_info : null
this.nextStep = nextStep
this.details = details
}
}
export function nextStepFor(
status: number,
message: string,
accountSidSecret: string,
authTokenSecret: string,
): string {
const sidUrl = accountSidSetupUrl(accountSidSecret)
const tokenUrl = authTokenSetupUrl(authTokenSecret)
const lower = message.toLowerCase()
if (status === 401 || lower.includes('authenticate')) {
return (
'Save ' +
accountSidSecret +
' and ' +
authTokenSecret +
' and approve host api.twilio.com. Account SID: ' +
sidUrl +
' Auth Token: ' +
tokenUrl
)
}
if (status === 403) {
return (
'This Twilio credential cannot access that resource. Confirm the Account SID belongs to the same account as the Auth Token, then retry. Secrets: ' +
sidUrl
)
}
if (status === 404) {
return (
'Twilio returned 404. Confirm the Account SID is saved as ' +
accountSidSecret +
' at ' +
sidUrl +
' and that the resource path exists.'
)
}
return (
'See https://www.twilio.com/docs/api/errors and confirm both secrets are saved with host api.twilio.com. Account SID: ' +
sidUrl +
' Auth Token: ' +
tokenUrl
)
}
export function resolvePageHref(pageUri: string): string {
if (pageUri.startsWith(API_ORIGIN + API_ACCOUNT_PREFIX)) return pageUri
if (pageUri.startsWith(API_ACCOUNT_PREFIX)) return API_ORIGIN + pageUri
throw new Error(
'pageUri must be a Twilio Accounts API path on https://api.twilio.com.',
)
}
export function assertSafeHref(href: string) {
if (!href.startsWith(API_ORIGIN + API_ACCOUNT_PREFIX)) {
throw new Error(
'Twilio requests must stay on https://api.twilio.com/2010-04-01/Accounts/.',
)
}
}
export type MutationPreview = {
dryRun: true
method: 'POST'
resource: string
host: string
body: Record<string, string>
accountSidSecret: string
authTokenSecret: string
}
export function mutationPreview(
input: TwilioAuthOptions & { dryRun?: unknown; confirm?: unknown },
resource: string,
body: Record<string, unknown>,
): MutationPreview | null {
if (!isDryRun(input)) return null
return {
dryRun: true,
method: 'POST',
resource,
host: API_HOST,
body: compactForm(body),
accountSidSecret: resolveAccountSidSecretName(input),
authTokenSecret: resolveAuthTokenSecretName(input),
}
}
/** Strip credential-like keys from a Twilio account payload before returning it. */
export function sanitizeAccount(record: JsonRecord) {
return {
sid: typeof record.sid === 'string' ? record.sid : null,
friendlyName:
typeof record.friendly_name === 'string' ? record.friendly_name : null,
status: typeof record.status === 'string' ? record.status : null,
type: typeof record.type === 'string' ? record.type : null,
dateCreated:
typeof record.date_created === 'string' ? record.date_created : null,
dateUpdated:
typeof record.date_updated === 'string' ? record.date_updated : null,
ownerAccountSid:
typeof record.owner_account_sid === 'string'
? record.owner_account_sid
: null,
}
}