feat: add toolbox contract shell and testkit

This commit is contained in:
2026-07-20 17:53:56 +02:00
commit ce9bddb218
53 changed files with 8950 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
import {
discoverToolboxCatalog,
type ToolboxDiscoveryOptions,
} from "./discovery.js";
import { parseToolboxApp, parseToolboxCatalog } from "./parse.js";
import {
ToolboxError,
type ResolvedToolboxCatalog,
type ToolboxAppManifest,
type ToolboxCatalog,
type ToolboxContextResult,
type ToolboxDiscoverySource,
} from "./types.js";
import { requireSameOrigin, resolveToolboxApp, resolveWebUrl } from "./urls.js";
export type ToolboxFetch = (
input: string | URL | Request,
init?: RequestInit,
) => Promise<Response>;
export interface ToolboxLoadOptions {
fetch?: ToolboxFetch;
signal?: AbortSignal;
baseUrl?: string | URL;
expectedOrigin?: string | URL;
}
function defaultFetch(): ToolboxFetch {
if (typeof globalThis.fetch !== "function") {
throw new ToolboxError(
"fetch-failed",
"No fetch implementation is available",
);
}
return globalThis.fetch.bind(globalThis);
}
function absoluteUrl(reference: string | URL, base?: string | URL): URL {
if (base !== undefined) return resolveWebUrl(reference, base);
if (typeof reference !== "string" || /^[a-z][a-z\d+.-]*:/i.test(reference)) {
return resolveWebUrl(reference);
}
if (typeof globalThis.location !== "undefined") {
return resolveWebUrl(reference, globalThis.location.href);
}
throw new ToolboxError(
"invalid-url",
`A base URL is required for relative URL: ${reference}`,
);
}
async function fetchJson(
url: URL,
options: ToolboxLoadOptions,
): Promise<unknown> {
let response: Response;
try {
response = await (options.fetch ?? defaultFetch())(url, {
headers: { accept: "application/json" },
redirect: "error",
...(options.signal === undefined ? {} : { signal: options.signal }),
});
} catch (cause) {
throw new ToolboxError("fetch-failed", `Could not fetch ${url.href}`, {
cause,
});
}
if (typeof response.url === "string" && response.url !== "") {
requireSameOrigin(
resolveWebUrl(response.url),
url,
`Final response for ${url.href}`,
);
}
if (!response.ok) {
throw new ToolboxError(
"fetch-failed",
`Could not fetch ${url.href}: HTTP ${response.status}`,
);
}
try {
return await response.json();
} catch (cause) {
throw new ToolboxError("fetch-failed", `${url.href} is not valid JSON`, {
cause,
});
}
}
export async function fetchToolboxAppManifest(
manifestUrl: string | URL,
options: ToolboxLoadOptions = {},
): Promise<ToolboxAppManifest> {
const url = absoluteUrl(manifestUrl, options.baseUrl);
if (options.expectedOrigin !== undefined) {
requireSameOrigin(
url,
absoluteUrl(options.expectedOrigin),
"Application manifest",
);
}
return parseToolboxApp(await fetchJson(url, options));
}
export async function fetchToolboxCatalog(
catalogUrl: string | URL,
options: ToolboxLoadOptions = {},
): Promise<ToolboxCatalog> {
const url = absoluteUrl(catalogUrl, options.baseUrl);
if (options.expectedOrigin !== undefined) {
requireSameOrigin(
url,
absoluteUrl(options.expectedOrigin),
"Toolbox catalog",
);
}
return parseToolboxCatalog(await fetchJson(url, options));
}
export async function loadToolboxCatalog(
catalogReference: string | URL,
options: ToolboxLoadOptions = {},
): Promise<ResolvedToolboxCatalog> {
const catalogUrl = absoluteUrl(catalogReference, options.baseUrl);
const expectedOrigin =
options.expectedOrigin === undefined
? catalogUrl
: absoluteUrl(options.expectedOrigin);
requireSameOrigin(catalogUrl, expectedOrigin, "Toolbox catalog");
const catalog = await fetchToolboxCatalog(catalogUrl, {
...options,
expectedOrigin,
});
const apps = await Promise.all(
catalog.apps
.filter((entry) => entry.enabled)
.map(async (entry) => {
if ("entry" in entry) {
return {
kind: "external" as const,
enabled: entry.enabled,
name: entry.name,
entryUrl: resolveWebUrl(entry.entry, catalogUrl),
launch: entry.launch,
};
}
const manifestUrl = requireSameOrigin(
resolveWebUrl(entry.manifest, catalogUrl),
expectedOrigin,
"Catalog application manifest",
);
const manifest = await fetchToolboxAppManifest(manifestUrl, {
...options,
expectedOrigin,
});
return {
kind: "manifest" as const,
enabled: entry.enabled,
app: resolveToolboxApp(manifest, manifestUrl, expectedOrigin),
};
}),
);
const homeUrl = requireSameOrigin(
resolveWebUrl(catalog.home, catalogUrl),
expectedOrigin,
"Toolbox home",
);
return {
catalog,
catalogUrl,
apps,
homeUrl,
};
}
export interface ToolboxContextLoadOptions
extends
ToolboxDiscoveryOptions,
Omit<ToolboxLoadOptions, "baseUrl" | "expectedOrigin"> {
catalogUrl?: string | URL;
}
function asToolboxError(error: unknown): ToolboxError {
return error instanceof ToolboxError
? error
: new ToolboxError(
"fetch-failed",
"The toolbox context could not be loaded",
{
cause: error,
},
);
}
export async function loadToolboxContext(
options: ToolboxContextLoadOptions = {},
): Promise<ToolboxContextResult> {
let source: ToolboxDiscoverySource;
let catalogUrl: URL;
let pageUrl: URL;
if (options.catalogUrl !== undefined) {
source = "explicit";
try {
pageUrl = resolveWebUrl(
options.location ??
(typeof globalThis.location === "undefined"
? options.catalogUrl
: globalThis.location.href),
);
catalogUrl = requireSameOrigin(
resolveWebUrl(options.catalogUrl, pageUrl),
pageUrl,
"Toolbox catalog",
);
} catch (error) {
return { status: "error", error: asToolboxError(error) };
}
} else {
const discovery = discoverToolboxCatalog(options);
if (discovery.status === "none") return { status: "standalone" };
if (discovery.status === "error") return discovery;
source = discovery.source;
catalogUrl = discovery.catalogUrl;
pageUrl = resolveWebUrl(
options.location ??
(typeof globalThis.location === "undefined"
? catalogUrl
: globalThis.location.href),
);
}
try {
const catalog = await loadToolboxCatalog(catalogUrl, {
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
...(options.signal === undefined ? {} : { signal: options.signal }),
expectedOrigin: pageUrl,
});
return { status: "ready", context: { source, catalog } };
} catch (error) {
return { status: "error", error: asToolboxError(error) };
}
}