Skip to content

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

Package listing

@kody/kit

src/safety.ts

73 lines · 2.3 KB · TypeScript
import type { DryRunResult, JsonRecord, MutationInput } from './types.ts'
import { requireString } from './types.ts'
import { KIT_API_BASE_URL, KIT_REQUIRED_HOST } from './setup.ts'

const ALLOWED_HOSTS = new Set(['api.kit.com'])

export function mutationPreview(
	input: MutationInput,
	preview: { method: string; path: string; body?: JsonRecord },
): DryRunResult | null {
	if (!input.dryRun) {
		if (input.confirm !== true) {
			throw new Error(
				`${preview.method} ${preview.path} requires confirm: true after explicit user approval, or dryRun: true.`,
			)
		}
		return null
	}
	return {
		dryRun: true,
		method: preview.method,
		path: preview.path,
		body: preview.body,
	}
}

export function rejectBroadcastSend(input: JsonRecord, action: string): void {
	if (input.send_at !== undefined && input.send_at !== null) {
		throw new Error(
			`${action} is draft-only and refuses send_at. Create or update a draft, then send from the Kit UI.`,
		)
	}
	if (input.status === 'published' || input.status === 'sent' || input.status === 'scheduled') {
		throw new Error(
			`${action} is draft-only and refuses status ${String(input.status)}. Send from the Kit UI.`,
		)
	}
}

export function normalizeKitPath(path: string): string {
	const trimmed = requireString(path, 'path')
	if (trimmed.startsWith('https://')) {
		const url = new URL(trimmed)
		if (!ALLOWED_HOSTS.has(url.host)) {
			throw new Error(`Kit requests must use host ${KIT_REQUIRED_HOST}. Got ${url.host}.`)
		}
		return `${url.pathname}${url.search}`
	}
	return trimmed.startsWith('/') ? trimmed : `/${trimmed}`
}

export function kitUrl(path: string, query?: JsonRecord): string {
	const normalized = normalizeKitPath(path)
	const url = new URL(normalized, `${KIT_API_BASE_URL.replace(/\/v4$/, '')}/`)
	if (!url.pathname.startsWith('/v4')) {
		url.pathname = `/v4${url.pathname === '/' ? '' : url.pathname}`
	}
	if (query) {
		for (const [key, value] of Object.entries(query)) {
			if (value === undefined || value === null) continue
			if (typeof value === 'boolean' || typeof value === 'number') {
				url.searchParams.set(key, String(value))
				continue
			}
			if (typeof value === 'string') {
				url.searchParams.set(key, value)
				continue
			}
			throw new Error(`query.${key} must be a string, number, or boolean.`)
		}
	}
	return url.toString()
}