← Public packages
@kody/api-research
Research third-party APIs: registry search, provider discovery, OpenAPI summarize, and client scaffold.
src/search-registry.ts
122 lines · 3.1 KB · TypeScriptimport {
BoundedBodyTooLargeError,
readBoundedBody,
} from './bounded-body.ts'
import { getErrorMessage } from './error-message.ts'
const INTEGRATIONS_SH_API_BASE = 'https://integrations.sh/api'
const MAX_SEARCH_BODY_BYTES = 500_000
const SEARCH_FETCH_TIMEOUT_MS = 10_000
export type RegistrySearchResult = {
domain: string
name: string
description: string
kinds: Array<string>
url: string
}
export type SearchRegistryInput = {
/** Provider name or domain to search for. */
query: string
/** Maximum number of results to return (1–20, default 5). */
limit?: number
}
export type SearchRegistryOutput = {
results: Array<RegistrySearchResult>
}
export function buildRegistrySearchUrl(query: string): string {
return `${INTEGRATIONS_SH_API_BASE}/search?q=${encodeURIComponent(query)}`
}
/**
* Search the public integrations.sh registry for a provider domain.
* Use when you need a canonical hostname before calling discover.
*
* @param input - Query string and optional result limit
* @returns Untrusted registry matches (domain, kinds, listing URL)
*
* @example
* import searchRegistry from 'kody:@kody/api-research/search-registry'
* const { results } = await searchRegistry({ query: 'github' })
*/
export default async function searchRegistry(
input: SearchRegistryInput,
): Promise<SearchRegistryOutput> {
const query = input.query.trim()
if (query.length === 0) {
throw new Error('query is required')
}
const limit = input.limit ?? 5
if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
throw new Error('limit must be an integer between 1 and 20')
}
const url = buildRegistrySearchUrl(query)
let response: Response
try {
response = await fetch(url, {
headers: { Accept: 'application/json' },
redirect: 'follow',
signal: AbortSignal.timeout(SEARCH_FETCH_TIMEOUT_MS),
})
} catch (cause) {
throw new Error(
`integrations.sh registry search failed: ${getErrorMessage(cause)}`,
)
}
if (!response.ok) {
throw new Error(
`integrations.sh registry search failed: HTTP ${response.status} for ${url}`,
)
}
let body: string
try {
body = await readBoundedBody(response, MAX_SEARCH_BODY_BYTES)
} catch (cause) {
if (cause instanceof BoundedBodyTooLargeError) {
throw new Error(
`integrations.sh registry search failed: ${cause.message}`,
)
}
throw cause
}
let parsed: unknown
try {
parsed = JSON.parse(body)
} catch (cause) {
throw new Error(
`integrations.sh registry search failed: invalid JSON (${getErrorMessage(cause)})`,
)
}
if (
typeof parsed !== 'object' ||
parsed === null ||
!Array.isArray((parsed as { results?: unknown }).results)
) {
throw new Error(
'integrations.sh registry search failed: unexpected response shape',
)
}
const results = (parsed as { results: Array<Record<string, unknown>> })
.results
.slice(0, limit)
.map((result) => ({
domain: String(result.domain ?? ''),
name: String(result.name ?? ''),
description: String(result.description ?? ''),
kinds: Array.isArray(result.kinds)
? result.kinds.filter((kind): kind is string => typeof kind === 'string')
: [],
url: String(result.url ?? ''),
}))
return { results }
}