Skip to content
← Public packages

@kody/integrations-sh

Client for the public integrations.sh registry: search, detect, cached surface, and live discover.

src/discover.ts

118 lines · 3.3 KB · TypeScript
import { fetchRegistryJson } from './client.ts'
import { normalizeProviderDomain } from './domain.ts'
import {
	hasUsableRegistryData,
	mapSurfaceDocument,
	type SurfaceDocument,
} from './types.ts'
import { buildDiscoverUrl, buildSurfaceUrl } from './urls.ts'

const SURFACE_FETCH_TIMEOUT_MS = 15_000
const LIVE_DISCOVER_FETCH_TIMEOUT_MS = 75_000
const CACHED_DISCOVERY_STALE_AFTER_MS = 30 * 24 * 60 * 60 * 1000

export type DiscoverInput = {
	/** Bare provider hostname (for example linear.app). */
	domain: string
	/** Skip the cache and always call live discover. */
	live?: boolean
}

export type DiscoverOutput = SurfaceDocument & {
	source: string
	provenance: 'cached' | 'live'
	liveDiscoveryError: string | null
}

function isCachedDiscoveryFresh(document: SurfaceDocument): boolean {
	if (document.discoveredAt == null) {
		return false
	}
	const discoveredAt = Date.parse(document.discoveredAt)
	if (Number.isNaN(discoveredAt)) {
		return false
	}
	return Date.now() - discoveredAt <= CACHED_DISCOVERY_STALE_AFTER_MS
}

/**
 * Load integrations.sh discovery for a provider domain.
 * Uses a fresh cached surface when available, otherwise live discover.
 *
 * @param input - Bare provider hostname, optional live=true
 * @returns Untrusted surfaces, credentials, and provenance
 *
 * @example
 * import discover from 'kody:@<username>/integrations-sh/discover'
 * const found = await discover({ domain: 'github.com' })
 */
export default async function discover(
	input: DiscoverInput,
): Promise<DiscoverOutput> {
	const domain = normalizeProviderDomain(input.domain)
	const surfaceUrl = buildSurfaceUrl(domain)
	const discoverUrl = buildDiscoverUrl(domain)

	const cached = input.live
		? null
		: await fetchRegistryJson(surfaceUrl, SURFACE_FETCH_TIMEOUT_MS)
	const cachedDocument =
		cached?.outcome === 'success'
			? mapSurfaceDocument(cached.json)
			: null
	const usableCached =
		cachedDocument != null && hasUsableRegistryData(cachedDocument)
			? cachedDocument
			: null

	if (usableCached != null && isCachedDiscoveryFresh(usableCached)) {
		return {
			...usableCached,
			source: surfaceUrl,
			provenance: 'cached',
			liveDiscoveryError: null,
		}
	}

	const live = await fetchRegistryJson(
		discoverUrl,
		LIVE_DISCOVER_FETCH_TIMEOUT_MS,
	)
	if (live.outcome === 'success') {
		const liveDocument = mapSurfaceDocument(live.json)
		if (liveDocument != null) {
			return {
				...liveDocument,
				source: discoverUrl,
				provenance: 'live',
				liveDiscoveryError: null,
			}
		}
	}

	const liveFailureMessage =
		live.outcome === 'not-found'
			? `HTTP 404 for ${discoverUrl}`
			: live.outcome === 'failure'
				? live.message
				: 'unexpected response shape'

	if (usableCached != null) {
		return {
			...usableCached,
			source: surfaceUrl,
			provenance: 'cached',
			liveDiscoveryError: `live discovery failed (${liveFailureMessage}); returning cached registry data discovered at ${usableCached.discoveredAt ?? 'an unknown time'}`,
		}
	}

	if (live.outcome === 'not-found' && cached?.outcome === 'not-found') {
		throw new Error(
			`Domain "${domain}" is not in the integrations.sh registry. Try search to find the canonical provider domain.`,
		)
	}

	throw new Error(
		`integrations.sh discover failed: ${liveFailureMessage}. No usable cached surface data is available for "${domain}".`,
	)
}