Skip to content
← Public packages

@kody/openapi

Bind an OpenAPI spec and call selected operations with saved integration or secret names.

src/url.ts

46 lines · 1.5 KB · TypeScript
import { normalizeApiBaseUrl } from './selection.ts'

export function buildOperationUrl(input: {
	apiBaseUrl: string
	path: string
	params: Record<string, string>
	query: Record<string, unknown>
}): URL {
	const base = normalizeApiBaseUrl(input.apiBaseUrl)
	let path = input.path.trim()
	path = path.replace(/\{([^}/]+)\}/g, (_match, name: string) => {
		if (!(name in input.params)) {
			throw new Error(
				`Missing required path parameter "${name}" for OpenAPI operation path ${input.path}.`,
			)
		}
		return encodeURIComponent(input.params[name] ?? '')
	})
	if (!path.startsWith('/') || path.startsWith('//')) {
		throw new Error(
			`OpenAPI operation path ${JSON.stringify(input.path)} must be relative to the binding apiBaseUrl (a single leading "/"). Spec paths never override the binding host.`,
		)
	}
	const url = new URL(`${base}${path}`)
	for (const [key, value] of Object.entries(input.query)) {
		if (value == null) continue
		if (Array.isArray(value)) {
			for (const entry of value) {
				if (entry == null) continue
				url.searchParams.append(key, String(entry))
			}
			continue
		}
		url.searchParams.append(key, String(value))
	}
	return url
}

export function assertPinnedToApiBaseUrl(url: URL, apiBaseUrl: string) {
	const expectedHost = new URL(normalizeApiBaseUrl(apiBaseUrl)).host
	if (url.host !== expectedHost) {
		throw new Error(
			`OpenAPI operation URL host "${url.host}" does not match binding apiBaseUrl host "${expectedHost}". Outbound requests are pinned to the binding apiBaseUrl; spec servers never widen host approval.`,
		)
	}
}