Skip to content

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

Package listing

@kody/kit

src/setup.ts

134 lines · 4.8 KB · TypeScript
import type { KitAuthInput, KitAuthMode } from './types.ts'
import { optionalString, requireString } from './types.ts'

export const KIT_API_BASE_URL = 'https://api.kit.com/v4'
export const KIT_AUTHORIZE_URL = 'https://api.kit.com/v4/oauth/authorize'
export const KIT_TOKEN_URL = 'https://api.kit.com/v4/oauth/token'
export const KIT_REQUIRED_HOSTS = ['api.kit.com'] as const
export const KIT_REQUIRED_HOST = 'api.kit.com'
export const KIT_CALLBACK_URL = 'https://kody.codes/connect/oauth'
export const KIT_OAUTH_APP_URL =
	'https://developers.kit.com/kit-app-store/building-apps#creating-your-app'
export const KIT_API_KEY_DASHBOARD_URL = 'https://app.kit.com/account_settings/developer_settings'
export const KIT_AUTH_DOCS_URL = 'https://developers.kit.com/api-reference/authentication'
export const DEFAULT_INTEGRATION_NAME = 'kit'
export const DEFAULT_API_KEY_SECRET = 'kitApiKey'
export const DEFAULT_OAUTH_SCOPES = ['public'] as const

export const KIT_SCOPES = {
	public: 'Default Kit App Store scope. Fine-grained scopes are not generally available yet.',
	read: 'Read access to Kit API v4 when the app requests it.',
	write: 'Write access to Kit API v4 when the app requests it.',
} as const

export function resolveIntegrationName(input: KitAuthInput = {}): string {
	const explicit =
		optionalString(input.integrationName, 'integrationName') ??
		optionalString(input.integration, 'integration')
	if (explicit) return explicit

	const account = optionalString(input.account, 'account')
	if (!account || account === 'default' || account === 'kit') {
		return DEFAULT_INTEGRATION_NAME
	}
	if (account.startsWith('kit-')) return account
	return `kit-${account}`
}

export function resolveSecretName(input: KitAuthInput = {}): string {
	const explicit = optionalString(input.secretName, 'secretName')
	if (explicit) return explicit
	const integrationName = resolveIntegrationName(input)
	if (integrationName === DEFAULT_INTEGRATION_NAME) return DEFAULT_API_KEY_SECRET
	const suffix = integrationName.startsWith('kit-')
		? integrationName.slice('kit-'.length)
		: integrationName
	return `${DEFAULT_API_KEY_SECRET}-${suffix}`
}

export function oauthConnectUrl(provider: string = DEFAULT_INTEGRATION_NAME): string {
	const name = requireString(provider, 'provider')
	const params = new URLSearchParams({
		provider: name,
		authorizeUrl: KIT_AUTHORIZE_URL,
		tokenUrl: KIT_TOKEN_URL,
		apiBaseUrl: KIT_API_BASE_URL,
		scopes: DEFAULT_OAUTH_SCOPES.join(' '),
		flow: 'confidential',
		pkce: 'false',
		allowedHosts: KIT_REQUIRED_HOSTS.join(','),
		dashboardUrl: KIT_OAUTH_APP_URL,
	})
	return `https://kody.codes/connect/oauth?${params.toString()}`
}

export function apiKeySetupUrl(secretName: string = DEFAULT_API_KEY_SECRET): string {
	const name = requireString(secretName, 'secretName')
	const params = new URLSearchParams({
		name,
		description: 'Kit v4 API key for subscribers, forms, tags, broadcasts, and sequences',
		allowedHosts: KIT_REQUIRED_HOSTS.join(','),
		scope: 'user',
	})
	return `https://kody.codes/account/secrets/new?${params.toString()}`
}

export function reconnectUrl(provider: string = DEFAULT_INTEGRATION_NAME): string {
	return `https://kody.codes/connect/oauth?provider=${encodeURIComponent(provider)}`
}

export function missingCredentialsMessage(options: {
	integrationName: string
	secretName: string
}): string {
	return [
		'Kit credentials are missing.',
		'API key (recommended for personal automation): create a v4 key at',
		KIT_API_KEY_DASHBOARD_URL,
		'then save it (do not paste the value in chat):',
		apiKeySetupUrl(options.secretName),
		'OAuth (Kit App Store apps): create an app at',
		KIT_OAUTH_APP_URL,
		`with redirect URI ${KIT_CALLBACK_URL}, then connect:`,
		oauthConnectUrl(options.integrationName),
		`Required API host: ${KIT_REQUIRED_HOST}. Auth docs: ${KIT_AUTH_DOCS_URL}.`,
	].join(' ')
}

export function nextStepForAuth(options: {
	authMode: KitAuthMode
	integrationName: string
	secretName: string
}): string {
	switch (options.authMode) {
		case 'oauth':
			return [
				`Reconnect the "${options.integrationName}" OAuth integration.`,
				reconnectUrl(options.integrationName),
				`Full BYO connect URL: ${oauthConnectUrl(options.integrationName)}`,
			].join(' ')
		case 'apiKey':
			return [
				`Save a Kit v4 API key as ${options.secretName} and approve host ${KIT_REQUIRED_HOST}.`,
				KIT_API_KEY_DASHBOARD_URL,
				apiKeySetupUrl(options.secretName),
			].join(' ')
		default: {
			const exhaustive: never = options.authMode
			throw new Error(`Unsupported Kit auth mode: ${String(exhaustive)}`)
		}
	}
}

export function isMutatingMethod(method: string): boolean {
	const normalized = method.toUpperCase()
	return (
		normalized === 'POST' ||
		normalized === 'PUT' ||
		normalized === 'PATCH' ||
		normalized === 'DELETE'
	)
}

export const OAUTH_CONNECT_URL = oauthConnectUrl()
export const API_KEY_SETUP_URL = apiKeySetupUrl()