Skip to content

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

Package listing

@kody/supabase

src/setup.ts

158 lines · 4.8 KB · TypeScript
import {
	optionalString,
	requireString,
	type SupabaseAuthInput,
} from './types.ts'

const ORIGIN = 'https://kody.codes'
const PAT_GENERATE_URL = 'https://supabase.com/dashboard/account/tokens'
const API_KEYS_URL = 'https://supabase.com/dashboard/project/_/settings/api-keys/'
const MANAGEMENT_HOST = 'api.supabase.com'

export const DEFAULT_PAT_SECRET = 'supabasePat'
export const DEFAULT_SERVICE_ROLE_SECRET = 'supabaseServiceRoleKey'

export function projectHost(projectRef: string): string {
	return `${projectRef}.supabase.co`
}

export function projectBaseUrl(projectRef: string): string {
	return `https://${projectHost(projectRef)}`
}

export function encodeQuery(params: Record<string, string>): string {
	return Object.entries(params)
		.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
		.join('&')
}

export function patSetupUrl(secretName = DEFAULT_PAT_SECRET): string {
	return `${ORIGIN}/account/secrets/new?${encodeQuery({
		name: secretName,
		description:
			'Supabase personal access token for the Management API (projects, orgs, generic /v1 paths)',
		allowedHosts: MANAGEMENT_HOST,
		scope: 'user',
	})}`
}

export function serviceRoleSetupUrl(input: {
	secretName?: string
	projectRef?: string
	projectHost?: string
} = {}): string {
	const secretName = input.secretName ?? DEFAULT_SERVICE_ROLE_SECRET
	const host = input.projectHost ?? (input.projectRef ? projectHost(input.projectRef) : undefined)
	const params: Record<string, string> = {
		name: secretName,
		description:
			'Supabase project service role or secret key for Auth admin, PostgREST, and Storage',
		scope: 'user',
	}
	if (host) params.allowedHosts = host
	return `${ORIGIN}/account/secrets/new?${encodeQuery(params)}`
}

export function setupUrls(input: SupabaseAuthInput = {}) {
	const names = resolveSecretNames(input)
	const host = input.projectUrl
		? new URL(input.projectUrl).host
		: input.projectRef
			? projectHost(input.projectRef)
			: undefined
	return {
		patGenerateUrl: PAT_GENERATE_URL,
		apiKeysUrl: API_KEYS_URL,
		patSetupUrl: patSetupUrl(names.patSecretName),
		serviceRoleSetupUrl: serviceRoleSetupUrl({
			secretName: names.serviceRoleSecretName,
			projectHost: host,
			projectRef: input.projectRef,
		}),
		managementHost: MANAGEMENT_HOST,
		projectHost: host,
	}
}

export function resolveAccountPurpose(account?: string): string | undefined {
	const trimmed = account?.trim()
	if (!trimmed || trimmed === 'default' || trimmed === 'supabase') return undefined
	if (trimmed.startsWith('supabase-')) return trimmed.slice('supabase-'.length)
	return trimmed
}

export function resolveSecretNames(input: SupabaseAuthInput): {
	patSecretName: string
	serviceRoleSecretName: string
	purpose?: string
} {
	const purpose = resolveAccountPurpose(input.account)
	const suffix = purpose ? `-${purpose}` : ''
	return {
		purpose,
		patSecretName:
			optionalString(input.patSecretName, 'patSecretName') ??
			optionalString(input.secretName, 'secretName') ??
			`${DEFAULT_PAT_SECRET}${suffix}`,
		serviceRoleSecretName:
			optionalString(input.serviceRoleSecretName, 'serviceRoleSecretName') ??
			`${DEFAULT_SERVICE_ROLE_SECRET}${suffix}`,
	}
}

export function projectAuth(input: SupabaseAuthInput): SupabaseAuthInput {
	return {
		account: input.account,
		projectRef: input.projectRef,
		projectUrl: input.projectUrl,
		patSecretName: input.patSecretName,
		serviceRoleSecretName: input.serviceRoleSecretName ?? input.secretName,
	}
}

export function managementAuth(input: SupabaseAuthInput): SupabaseAuthInput {
	return {
		account: input.account,
		projectRef: input.projectRef,
		projectUrl: input.projectUrl,
		secretName: input.secretName,
		patSecretName: input.patSecretName,
		serviceRoleSecretName: input.serviceRoleSecretName,
	}
}

export function assertSecretName(name: string, label: string): string {
	const trimmed = requireString(name, label)
	if (!/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(trimmed)) {
		throw new Error(
			`${label} must be a secret identifier (letters, digits, underscore, hyphen).`,
		)
	}
	return trimmed
}

export function secretPlaceholder(name: string): string {
	return `{{secret:${assertSecretName(name, 'secretName')}}}`
}

/**
 * Return prefilled Supabase secret setup URLs.
 * @example
 * import setup from 'kody:@kody/supabase/setup'
 * const urls = setup({ account: 'work', projectRef: 'abcdefghijklmnop' })
 */
export default function setupEntrypoint(
	input: SupabaseAuthInput & Record<string, unknown> = {},
) {
	return setupUrls({
		account: optionalString(input.account, 'account'),
		secretName: optionalString(input.secretName, 'secretName'),
		patSecretName: optionalString(input.patSecretName, 'patSecretName'),
		serviceRoleSecretName: optionalString(
			input.serviceRoleSecretName,
			'serviceRoleSecretName',
		),
		projectRef: optionalString(input.projectRef, 'projectRef'),
		projectUrl: optionalString(input.projectUrl, 'projectUrl'),
	})
}