Skip to content

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

Package listing

@kody/twilio

src/smoke-test.ts

129 lines · 3.3 KB · TypeScript
import { kody } from 'kody:runtime'
import { getAccount } from './account.ts'
import {
	API_HOST,
	accountSidSetupUrl,
	authTokenSetupUrl,
	resolveAccountSidSecretName,
	resolveAuthTokenSecretName,
	type TwilioAuthOptions,
} from './validation.ts'
import { listIncomingNumbers } from './incoming-numbers.ts'
import { listMessages } from './messages.ts'
import { sendMessage } from './send-message.ts'

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 : []
}

async function listUserSecretNames(): Promise<Set<string>> {
	try {
		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
	} catch {
		return new Set()
	}
}

/**
 * Local helper checks, plus a live account read when both secrets exist.
 *
 * Without secrets this still returns `{ ok: true, live: false }` and the
 * prefilled setup URLs so the package is forkable. With secrets it proves
 * host approval via account lookup — it never sends SMS.
 *
 * @example
 * import smokeTest from 'kody:@kody/twilio/smoke-test'
 * const result = await smokeTest()
 */
export async function smokeTest(params: TwilioAuthOptions = {}) {
	const preview = await sendMessage({
		...params,
		to: '+15555550100',
		from: '+15555550101',
		body: 'kody twilio dry-run smoke',
		dryRun: true,
	})
	if (!('dryRun' in preview) || preview.dryRun !== true) {
		throw new Error('sendMessage dry-run self-check failed.')
	}

	const accountSidSecret = resolveAccountSidSecretName(params)
	const authTokenSecret = resolveAuthTokenSecretName(params)
	const names = await listUserSecretNames()
	const hasAccountSid = names.has(accountSidSecret)
	const hasAuthToken = names.has(authTokenSecret)
	const setup = {
		secrets: {
			[accountSidSecret]: hasAccountSid,
			[authTokenSecret]: hasAuthToken,
		},
		nextSteps: [
			'Save the Account SID. Do not paste it into chat.',
			accountSidSetupUrl(accountSidSecret),
			'Save the Auth Token. Do not paste it into chat.',
			authTokenSetupUrl(authTokenSecret),
			'Approve host api.twilio.com for both secrets.',
		],
	}

	if (!hasAccountSid || !hasAuthToken) {
		return {
			ok: true,
			live: false,
			host: API_HOST,
			selfCheck: { dryRunSend: true },
			accountSidSecret,
			authTokenSecret,
			setup,
		}
	}

	const [account, numbers, messages] = await Promise.all([
		getAccount(params),
		listIncomingNumbers({ ...params, pageSize: 1 }),
		listMessages({ ...params, pageSize: 1 }),
	])

	return {
		ok: true,
		live: true,
		host: API_HOST,
		selfCheck: { dryRunSend: true },
		accountSidSecret,
		authTokenSecret,
		account: {
			sid: account.account.sid,
			status: account.account.status,
			type: account.account.type,
			friendlyName: account.account.friendlyName,
		},
		incomingNumberCount: numbers.items.length,
		messageSampleCount: messages.items.length,
		setup: {
			secrets: {
				[accountSidSecret]: true,
				[authTokenSecret]: true,
			},
			nextSteps: [],
		},
	}
}

export default smokeTest