Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/twilio

src/helpers.ts

158 lines · 4.1 KB · TypeScript
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,
	}
}