feat: release toolbox portal 0.1.0

This commit is contained in:
2026-07-20 18:40:51 +02:00
commit 5d2e466ad6
55 changed files with 11127 additions and 0 deletions

143
src/catalogue.ts Normal file
View File

@@ -0,0 +1,143 @@
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<LoadedCatalogue> {
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<string>();
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);
}