Skip to content

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

Package listing

@kody/resend

src/oauth-setup.ts

92 lines · 2.8 KB · TypeScript
/**
 * Optional OAuth setup. Resend implements OAuth 2.1 with PKCE and Dynamic
 * Client Registration, so a public client can register itself — no dashboard
 * app is required. API keys are the default auth lane for this package.
 *
 * https://resend.com/docs/guides/building-a-resend-oauth-client
 */
import {
	API_BASE_URL,
	OAUTH_SCOPES,
	ResendApiError,
	oauthConnectUrl,
} from './resend-core.ts'

export type RegisterOauthClientInput = {
	/** Must be exactly `https://kody.codes/connect/oauth` for hosted Kody OAuth. */
	redirectUri: string
	clientName?: string
	scope?: string
}

/**
 * POST /oauth/register — dynamically register a public OAuth client.
 * Unauthenticated. Defaults to `full_access` so every helper works.
 */
export async function registerOauthClient(
	input: RegisterOauthClientInput,
	options: { fetchImpl?: typeof fetch } = {},
) {
	if (!input?.redirectUri) {
		throw new Error(
			'registerOauthClient: redirectUri is required (for Kody hosted OAuth use https://kody.codes/connect/oauth)',
		)
	}
	const fetchImpl = options.fetchImpl ?? fetch
	const response = await fetchImpl(API_BASE_URL + '/oauth/register', {
		method: 'POST',
		headers: { 'content-type': 'application/json' },
		body: JSON.stringify({
			client_name: input.clientName ?? 'Kody',
			redirect_uris: [input.redirectUri],
			grant_types: ['authorization_code', 'refresh_token'],
			response_types: ['code'],
			token_endpoint_auth_method: 'none',
			scope: input.scope ?? OAUTH_SCOPES.fullAccess,
		}),
	})
	const text = await response.text()
	let parsed: unknown = null
	try {
		parsed = text ? JSON.parse(text) : null
	} catch {
		parsed = text
	}
	if (!response.ok) {
		const record = (parsed ?? {}) as { message?: string; name?: string }
		throw new ResendApiError(
			record.message
				? (record.name ? record.name + ': ' : '') + record.message
				: 'Resend API ' + response.status + ' POST /oauth/register',
			{ status: response.status, body: parsed, method: 'POST', path: '/oauth/register' },
		)
	}
	const record = (parsed ?? {}) as {
		client_id?: string
		client_name?: string
		redirect_uris?: string[]
		scope?: string
	}
	if (!record.client_id) {
		throw new ResendApiError('registerOauthClient: response had no client_id', {
			body: parsed,
			method: 'POST',
			path: '/oauth/register',
		})
	}
	return {
		client_id: record.client_id,
		client_name: record.client_name ?? '',
		redirect_uris: record.redirect_uris ?? [],
		scope: record.scope ?? '',
		connectUrl: oauthConnectUrl(undefined, input.scope ?? OAUTH_SCOPES.fullAccess),
	}
}

/**
 * Hosted `/connect/oauth` URL for Resend (PKCE, no client secret).
 * Defaults to https://kody.codes and the `full_access` scope.
 */
export function buildConnectUrl(input: { provider?: string; scope?: string } = {}) {
	return oauthConnectUrl(input.provider ?? 'resend', input.scope ?? OAUTH_SCOPES.fullAccess)
}