Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@kody/blandPublic

src/smoke-test.ts

97 lines · 2.7 KB · TypeScript
/**
 * Safe Bland setup check: secret metadata + dry-run send-call preview.
 * Pass `live: true` to also call `GET /v1/me` (read-only auth proof).
 *
 * @param input - optional live flag and auth overrides
 * @returns Setup status and next steps
 *
 * @example
 * import smokeTest from 'kody:@kody/bland/smoke-test'
 * await smokeTest({ live: true })
 */
import { kody } from 'kody:runtime'
import {
	API_HOST,
	blandRequest,
	resolveApiKeySecretName,
	setupUrls,
	type BlandAuthInput,
} from './core.ts'
import { readBlandSettings } from './settings.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
	}
	if (Array.isArray(result)) return result as Array<{ name?: string; scope?: string }>
	return []
}

export type SmokeTestInput = BlandAuthInput & {
	/** When true, also GET /v1/me (requires saved key + host approval). */
	live?: boolean
}

export default async function smokeTest(input: SmokeTestInput = {}) {
	const settings = await readBlandSettings()
	const apiKeySecret = resolveApiKeySecretName(input)
	const names = new Set<string>()
	try {
		const listed = await kody.secretList({ scope: 'user' })
		for (const entry of secretEntries(listed)) {
			if (entry?.name) names.add(String(entry.name))
		}
	} catch {
		/* listing can be empty in some contexts */
	}
	const hasApiKey = names.has(apiKeySecret)

	const preview = await blandRequest({
		...input,
		method: 'POST',
		path: '/v1/calls',
		body: {
			phone_number: '+15551234567',
			task: 'Smoke-test preview only — do not dial.',
			voice: settings.defaultVoice ?? 'maya',
			max_duration: 1,
		},
		dryRun: true,
	})

	const result: Record<string, unknown> = {
		ok: hasApiKey,
		hasApiKey,
		apiKeySecret,
		settings: { defaultVoice: settings.defaultVoice },
		sendCallPreview: preview,
		setup: setupUrls(input),
		approveHost: API_HOST,
		next: [] as string[],
	}

	const next = result.next as string[]
	if (!hasApiKey) {
		next.push('Save Bland API key: ' + settings.apiKeyUrl)
		next.push('Approve host ' + API_HOST + ' on that secret.')
	} else {
		next.push(
			'Try auth: import me from "kody:@kody/bland/me"; await me()',
		)
		next.push(
			'Dry-run a call: import { sendCall } from "kody:@kody/bland/send-call"; await sendCall({ phone_number, task })',
		)
	}

	if (input.live) {
		try {
			result.me = await blandRequest({ ...input, method: 'GET', path: '/v1/me' })
			result.ok = true
		} catch (error) {
			result.liveError = error instanceof Error ? error.message : String(error)
			result.ok = false
		}
	}

	return result
}