import { contextualizeToolboxLink, fetchToolboxAppManifest, fetchToolboxCatalog, requireSameOrigin, resolveToolboxApp, resolveWebUrl, type ToolboxCatalogEntry, type ToolboxCatalogExternalEntry, type ToolboxCatalogManifestEntry, } from '@add-ideas/toolbox-contract'; import type { LoadedCatalogue, ResolvedApp, UnavailableApp } from './types'; function isInternal( entry: ToolboxCatalogEntry ): entry is ToolboxCatalogManifestEntry { return 'manifest' in entry; } function fallbackId(entry: ToolboxCatalogExternalEntry, entryUrl: URL): string { const slug = entry.name .toLocaleLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); let hash = 2166136261; for (const character of `${entry.name}\0${entryUrl.href}`) { hash ^= character.codePointAt(0) ?? 0; hash = Math.imul(hash, 16777619); } return `external:${slug || 'tool'}:${(hash >>> 0).toString(36)}`; } export async function loadCatalogue( catalogueHref: string, signal?: AbortSignal ): Promise { const pageUrl = resolveWebUrl(document.baseURI); const catalogueUrl = requireSameOrigin( resolveWebUrl(catalogueHref, pageUrl), pageUrl, 'Toolbox catalogue' ); const catalogue = await fetchToolboxCatalog(catalogueUrl, { signal, expectedOrigin: pageUrl, }); const enabled = catalogue.apps.filter((entry) => entry.enabled); const results = await Promise.all( enabled.map( async ( entry ): Promise<{ app?: ResolvedApp; unavailable?: UnavailableApp }> => { if (!isInternal(entry)) { const entryUrl = resolveWebUrl(entry.entry, catalogueUrl); return { app: { id: fallbackId(entry, entryUrl), name: entry.name, description: 'External tool', entryUrl: entryUrl.href, categories: [], tags: [], launchModes: [entry.launch], isExternal: true, }, }; } const manifestUrl = requireSameOrigin( resolveWebUrl(entry.manifest, catalogueUrl), pageUrl, 'Application manifest' ); try { const manifest = await fetchToolboxAppManifest(manifestUrl, { signal, expectedOrigin: pageUrl, }); const resolved = resolveToolboxApp(manifest, manifestUrl, pageUrl); return { app: { id: manifest.id, name: manifest.name, version: manifest.version, description: manifest.description, entryUrl: resolved.entryUrl.href, iconUrl: resolved.iconUrl.href, categories: manifest.categories, tags: manifest.tags, launchModes: manifest.integration.launchModes, isExternal: false, manifestUrl: resolved.manifestUrl.href, sourceUrl: manifest.source?.repository, requirements: manifest.requirements, privacy: manifest.privacy, }, }; } catch (error) { if (signal?.aborted) throw error; return { unavailable: { id: `unavailable:${manifestUrl.href}`, name: manifestUrl.pathname.split('/').at(-2) ?? 'Unknown app', manifestUrl: manifestUrl.href, reason: error instanceof Error ? error.message : 'Unknown manifest error.', }, }; } } ) ); const apps = results.flatMap((result) => (result.app ? [result.app] : [])); const ids = new Set(); for (const app of apps) { if (ids.has(app.id)) throw new Error(`Duplicate app id: ${app.id}.`); ids.add(app.id); } return { catalogue, catalogueUrl, homeUrl: requireSameOrigin( resolveWebUrl(catalogue.home, catalogueUrl), pageUrl, 'Toolbox home' ).href, apps, unavailable: results.flatMap((result) => result.unavailable ? [result.unavailable] : [] ), }; } export function contextualLaunchUrl( entryHref: string, catalogueHref: string ): string { return contextualizeToolboxLink(entryHref, catalogueHref); }