Skip to content
← Public packages

@kody/openapi

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

src/bind.ts

99 lines · 3.3 KB · TypeScript
import { assertOpenApiBoundAuth, type OpenApiBoundAuth } from './auth.ts'
import { fetchOpenApiSpecText } from './fetch-spec-lib.ts'
import { assertHttpsUrl } from './https.ts'
import { parseOpenApiSpec } from './parse-spec-lib.ts'
import {
	assertBindingName,
	assertOpenApiBindingWithinSizeLimit,
	normalizeApiBaseUrl,
	normalizeSelection,
	resolveOpenApiSelection,
	toOpenApiBindingSummary,
	type OpenApiBinding,
	type OpenApiBindingSelection,
	type OpenApiBindingSummary,
} from './selection.ts'
import { loadBindings, saveBindings } from './storage.ts'

export type BindInput = {
	/** Binding key used later in call({ name }). */
	name: string
	/** HTTPS URL of the OpenAPI 3.x document. */
	specUrl: string
	/** HTTPS API origin operations are pinned to. Spec servers never override this host. */
	apiBaseUrl: string
	/** Credential names only — never raw tokens. */
	auth: OpenApiBoundAuth
	/** At least one of operationIds, tags, or pathPrefixes. Max 100 operations. */
	selection: OpenApiBindingSelection
	/** When false (default), DELETE operations are excluded even if selected. */
	includeDestructive?: boolean
	description?: string | null
	/** Skip the spec fetch when you already have the document text. */
	specText?: string
}

export type BindOutput = {
	binding: OpenApiBindingSummary
	operationSlugs: Array<string>
	warnings: Array<string>
}

/**
 * Create or replace a named OpenAPI binding in this package's storage.
 * Use after community_fork of @kody/openapi, when you want reusable
 * call({ name, operation }) instead of a one-off client.
 *
 * @param input - Spec URL, api base, auth names, and a narrow selection
 * @returns Binding summary, selected slugs, and parse/selection warnings
 *
 * @example
 * import bind from 'kody:@<username>/openapi/bind'
 * await bind({
 *   name: 'acme',
 *   specUrl: 'https://api.example.com/openapi.json',
 *   apiBaseUrl: 'https://api.example.com',
 *   auth: { kind: 'integration', provider: 'acme' },
 *   selection: { pathPrefixes: ['/widgets'] },
 * })
 */
export default async function bind(input: BindInput): Promise<BindOutput> {
	const name = assertBindingName(input.name)
	const specUrl = assertHttpsUrl(input.specUrl, 'specUrl').toString()
	const apiBaseUrl = normalizeApiBaseUrl(
		assertHttpsUrl(input.apiBaseUrl, 'apiBaseUrl').toString(),
	)
	const auth = assertOpenApiBoundAuth(input.auth)
	const selection = normalizeSelection(input.selection)
	const includeDestructive = input.includeDestructive === true
	const specText =
		input.specText ?? (await fetchOpenApiSpecText({ specUrl }))
	const parsed = parseOpenApiSpec(specText)
	const resolved = resolveOpenApiSelection({
		operations: parsed.operations,
		selection,
		includeDestructive,
	})
	const binding: OpenApiBinding = {
		name,
		specUrl,
		apiBaseUrl,
		description: input.description ?? null,
		auth,
		selection,
		includeDestructive,
		specTitle: parsed.title,
		specVersion: parsed.version,
		operations: resolved.operations,
		updatedAt: new Date().toISOString(),
	}
	assertOpenApiBindingWithinSizeLimit(binding)
	const bindings = await loadBindings()
	bindings[name] = binding
	await saveBindings(bindings)
	return {
		binding: toOpenApiBindingSummary(binding),
		operationSlugs: binding.operations.map((operation) => operation.slug),
		warnings: [...parsed.warnings, ...resolved.warnings],
	}
}