Skip to content

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

Package listing

@kody/x

src/accounts.ts

136 lines · 4.5 KB · TypeScript
import { kody } from 'kody:runtime'
import { DEFAULT_INTEGRATION, connectUrls } from './domain.ts'
import type { ResolvedXOAuth, XAccountParams, XAccountSummary } from './types.ts'

function normalizeIntegrationName(name: string): string {
	return name.trim().toLowerCase()
}

/**
 * Resolve which OAuth integration to use.
 *
 * - `integration` wins when set (exact name, for example `x-work`)
 * - `account: 'work'` → `x-work`
 * - `account: 'default'` / `'x'` / omitted → `x`
 * - values that already start with `x-` are used as-is
 */
export function resolveIntegrationName(params: XAccountParams = {}): string {
	if (typeof params.integration === 'string' && params.integration.trim()) {
		return normalizeIntegrationName(params.integration)
	}
	if (typeof params.account === 'string' && params.account.trim()) {
		const account = normalizeIntegrationName(params.account)
		if (account === 'default' || account === 'x') return DEFAULT_INTEGRATION
		if (account.startsWith('x-')) return account
		return `x-${account}`
	}
	return DEFAULT_INTEGRATION
}

export function accountAliasForIntegration(integrationName: string): string | null {
	const name = normalizeIntegrationName(integrationName)
	if (name === DEFAULT_INTEGRATION) return null
	if (name.startsWith('x-')) return name.slice(2)
	return name
}

export async function resolveOAuthAccount(
	params: XAccountParams = {},
): Promise<ResolvedXOAuth> {
	const integrationName = resolveIntegrationName(params)
	const got = await kody.integration_get({ name: integrationName })
	const cfg = (
		got && typeof got === 'object' && 'integration' in got
			? (got as { integration: Record<string, unknown> | null }).integration
			: got
	) as Record<string, unknown> | null

	if (!cfg || typeof cfg !== 'object') {
		const urls = connectUrls(integrationName)
		throw new Error(
			`No X OAuth integration named "${integrationName}". Connect it at ${urls.firstTime}`,
		)
	}

	const accessTokenSecretName =
		typeof cfg.accessTokenSecretName === 'string' ? cfg.accessTokenSecretName : ''
	const refreshTokenSecretName =
		typeof cfg.refreshTokenSecretName === 'string' ? cfg.refreshTokenSecretName : null
	const clientId = typeof cfg.clientId === 'string' ? cfg.clientId : null

	if (!accessTokenSecretName) {
		throw new Error(
			`X integration "${integrationName}" is missing an access token secret name. Reconnect at ${connectUrls(integrationName).reconnect}`,
		)
	}

	return {
		integrationName,
		account: accountAliasForIntegration(integrationName),
		accessTokenSecretName,
		refreshTokenSecretName,
		clientId,
	}
}

export async function listXAccounts(): Promise<{
	accounts: XAccountSummary[]
	defaultIntegration: string
	connect: ReturnType<typeof connectUrls>
}> {
	const listed = await kody.integration_list({})
	const items = Array.isArray(listed)
		? listed
		: Array.isArray((listed as { integrations?: unknown[] })?.integrations)
			? (listed as { integrations: unknown[] }).integrations
			: Array.isArray((listed as { items?: unknown[] })?.items)
				? (listed as { items: unknown[] }).items
				: []

	const accounts = items
		.map((item) => {
			const name =
				item && typeof item === 'object' && typeof (item as { name?: unknown }).name === 'string'
					? normalizeIntegrationName((item as { name: string }).name)
					: ''
			if (!name || (name !== DEFAULT_INTEGRATION && !name.startsWith('x-'))) return null
			const accessTokenSecretName =
				item &&
				typeof item === 'object' &&
				typeof (item as { accessTokenSecretName?: unknown }).accessTokenSecretName === 'string'
					? (item as { accessTokenSecretName: string }).accessTokenSecretName
					: null
			const alias = accountAliasForIntegration(name)
			return {
				account: alias,
				integration: name,
				default: name === DEFAULT_INTEGRATION,
				accessTokenSecretName,
				useWhen:
					name === DEFAULT_INTEGRATION
						? 'Default X account. Use when the caller does not name another connected account.'
						: `Use when the caller asks for the ${alias} X account (integration ${name}).`,
			} satisfies XAccountSummary
		})
		.filter((entry): entry is XAccountSummary => entry !== null)
		.sort(
			(a, b) =>
				Number(b.default) - Number(a.default) || a.integration.localeCompare(b.integration),
		)

	return {
		accounts,
		defaultIntegration: DEFAULT_INTEGRATION,
		connect: connectUrls(),
	}
}

/**
 * List connected `x` / `x-*` OAuth integrations.
 * @example
 * import listXAccounts from 'kody:@kody/x/accounts'
 * const { accounts } = await listXAccounts()
 */
export default async function listXAccountsEntrypoint() {
	return await listXAccounts()
}