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,288 @@
import {
ToolboxValidationError,
contextualizeToolboxLink,
discoverToolboxCatalog,
loadToolboxContext,
parseToolboxApp,
parseToolboxCatalog,
type ToolboxAppManifest,
} from "../src/index.js";
import { describe, expect, it, vi } from "vitest";
const app = (
overrides: Record<string, unknown> = {},
): Record<string, unknown> => ({
schemaVersion: 1,
id: "de.add-ideas.pdf-tools",
name: "PDF Workbench",
version: "1.2.3",
description: "Local PDF tools",
entry: "./index.html",
icon: "./icon.svg",
categories: ["documents", "pdf"],
tags: ["merge", "split"],
integration: {
contextVersion: 1,
launchModes: ["navigate", "new-tab"],
embedding: "unsupported",
},
requirements: {
secureContext: true,
workers: true,
indexedDb: true,
crossOriginIsolated: false,
},
privacy: {
processing: "local",
fileUploads: false,
telemetry: false,
},
source: {
repository: "https://git.example.test/pdf-tools",
license: "AGPL-3.0-only",
},
...overrides,
});
const catalog = (
overrides: Record<string, unknown> = {},
): Record<string, unknown> => ({
schemaVersion: 1,
id: "de.add-ideas.toolbox",
name: "Toolbox",
home: "./",
theme: { mode: "system", brand: "add·ideas" },
apps: [{ manifest: "./pdf/toolbox-app.json", enabled: true }],
...overrides,
});
const json = (value: unknown, init: ResponseInit = {}): Response =>
new Response(JSON.stringify(value), {
status: 200,
headers: { "content-type": "application/json" },
...init,
});
describe("v1 runtime parsing", () => {
it("parses the full app contract and drops unknown optional fields", () => {
const parsed = parseToolboxApp(
app({
futureField: "ignored",
privacy: {
processing: "local",
fileUploads: false,
telemetry: false,
futurePrivacyField: true,
},
}),
);
expect(parsed).toMatchObject({
schemaVersion: 1,
id: "de.add-ideas.pdf-tools",
privacy: { processing: "local", fileUploads: false, telemetry: false },
});
expect(parsed).not.toHaveProperty("futureField");
expect(parsed.privacy).not.toHaveProperty("futurePrivacyField");
});
it("rejects unsupported versions and malformed known optional fields", () => {
expect(() => parseToolboxApp(app({ schemaVersion: 2 }))).toThrow(
ToolboxValidationError,
);
expect(() =>
parseToolboxApp(
app({
requirements: {
secureContext: true,
workers: true,
indexedDb: true,
crossOriginIsolated: false,
topLevelContext: "sometimes",
},
}),
),
).toThrow(/topLevelContext must be a boolean/u);
});
it("accepts omitted source metadata but validates it when supplied", () => {
const withoutSource = app();
delete withoutSource.source;
expect(parseToolboxApp(withoutSource)).not.toHaveProperty("source");
expect(() =>
parseToolboxApp(
app({ source: { repository: "../repository", license: "MIT" } }),
),
).toThrow(/absolute HTTP\(S\) URL/u);
});
it("parses manifest references and external inline catalog entries", () => {
const parsed = parseToolboxCatalog(
catalog({
futureField: 1,
apps: [
{ manifest: "./pdf/toolbox-app.json", enabled: true, future: true },
{
name: "External docs",
entry: "https://docs.example.test/",
launch: "new-tab",
enabled: true,
},
],
}),
);
expect(parsed.apps).toHaveLength(2);
expect(parsed.apps[1]).toEqual({
name: "External docs",
entry: "https://docs.example.test/",
launch: "new-tab",
enabled: true,
});
expect(parsed).not.toHaveProperty("futureField");
});
});
describe("same-origin discovery", () => {
it("prefers a same-origin query context over meta context", () => {
const result = discoverToolboxCatalog({
location: "https://tools.example.test/pdf/?toolbox=%2Fcatalog.json",
document: {
querySelector: () => ({
getAttribute: () => "/meta-catalog.json",
}),
},
});
expect(result.status).toBe("found");
if (result.status === "found") {
expect(result.source).toBe("query");
expect(result.catalogUrl.href).toBe(
"https://tools.example.test/catalog.json",
);
}
});
it("rejects a cross-origin query without falling through to meta", () => {
const result = discoverToolboxCatalog({
location:
"https://tools.example.test/pdf/?toolbox=https%3A%2F%2Fevil.test%2Fcatalog.json",
document: {
querySelector: () => ({ getAttribute: () => "/safe.json" }),
},
});
expect(result.status).toBe("error");
if (result.status === "error")
expect(result.error.code).toBe("cross-origin");
});
});
describe("loading and links", () => {
it("loads a catalog, resolves manifests and URLs, and supports external apps", async () => {
const fetch = vi.fn(
async (input: string | URL | Request, init?: RequestInit) => {
void init;
const url = new URL(input instanceof Request ? input.url : input);
if (url.pathname === "/catalog.json") {
return json(
catalog({
apps: [
{ manifest: "./pdf/toolbox-app.json", enabled: true },
{ manifest: "./disabled/toolbox-app.json", enabled: false },
{
name: "Documentation",
entry: "https://docs.example.test/",
launch: "new-tab",
enabled: true,
},
],
}),
);
}
if (url.pathname === "/pdf/toolbox-app.json") return json(app());
if (url.pathname === "/disabled/toolbox-app.json") {
throw new Error("Disabled manifests must not be fetched");
}
return new Response("Not found", { status: 404 });
},
);
const result = await loadToolboxContext({
location: "https://tools.example.test/pdf/?toolbox=/catalog.json",
fetch,
});
for (const [, init] of fetch.mock.calls) {
expect(init).toMatchObject({ redirect: "error" });
}
expect(result.status).toBe("ready");
if (result.status === "ready") {
expect(result.context.catalog.homeUrl.href).toBe(
"https://tools.example.test/",
);
expect(result.context.catalog.apps[0]?.kind).toBe("manifest");
const first = result.context.catalog.apps[0];
if (first?.kind === "manifest") {
expect(first.app.entryUrl.href).toBe(
"https://tools.example.test/pdf/index.html",
);
}
expect(result.context.catalog.apps[1]).toMatchObject({
kind: "external",
launch: "new-tab",
});
}
});
it("returns a graceful error result for invalid JSON and fetch failure", async () => {
const invalid = await loadToolboxContext({
location: "https://tools.example.test/?toolbox=/catalog.json",
fetch: async () => json(catalog({ schemaVersion: 2 })),
});
expect(invalid.status).toBe("error");
const unavailable = await loadToolboxContext({
location: "https://tools.example.test/?toolbox=/catalog.json",
fetch: async () => new Response("No", { status: 503 }),
});
expect(unavailable.status).toBe("error");
if (unavailable.status === "error") {
expect(unavailable.error.code).toBe("fetch-failed");
}
});
it("rejects a fetch redirected to a cross-origin response URL", async () => {
const response = json(catalog());
Object.defineProperty(response, "url", {
value: "https://evil.example/toolbox.catalog.json",
});
const result = await loadToolboxContext({
location: "https://tools.example.test/?toolbox=/catalog.json",
fetch: async () => response,
});
expect(result.status).toBe("error");
if (result.status === "error")
expect(result.error.code).toBe("cross-origin");
});
it("adds context to same-origin links but leaves external links alone", () => {
expect(
new URL(
contextualizeToolboxLink("./other/", "/catalog.json", {
location: "https://tools.example.test/pdf/",
}),
).searchParams.get("toolbox"),
).toBe("https://tools.example.test/catalog.json");
expect(
contextualizeToolboxLink("https://docs.example.test/", "/catalog.json", {
location: "https://tools.example.test/pdf/",
}),
).toBe("https://docs.example.test/");
});
});
export const validAppFixture = app() as unknown as ToolboxAppManifest;

View File

@@ -0,0 +1,261 @@
// @vitest-environment node
import { readFileSync } from "node:fs";
import Ajv2020, { type ValidateFunction } from "ajv/dist/2020.js";
import { describe, expect, it } from "vitest";
import { parseToolboxApp, parseToolboxCatalog } from "../src/index.js";
const appSchema = JSON.parse(
readFileSync(
new URL("../../../schemas/toolbox-app.v1.schema.json", import.meta.url),
"utf8",
),
) as object;
const catalogSchema = JSON.parse(
readFileSync(
new URL("../../../schemas/toolbox-catalog.v1.schema.json", import.meta.url),
"utf8",
),
) as object;
const ajv = new Ajv2020({
allErrors: true,
strict: true,
// `not: { required: [...] }` intentionally names the opposite union arm.
strictRequired: false,
});
const validateApp = ajv.compile(appSchema);
const validateCatalog = ajv.compile(catalogSchema);
type Parser = (value: unknown) => unknown;
function runtimeAccepts(parser: Parser, value: unknown): boolean {
try {
parser(value);
return true;
} catch {
return false;
}
}
function expectParity(
validate: ValidateFunction,
parser: Parser,
value: unknown,
expected: boolean,
): void {
const schemaAccepted = validate(value);
expect(
schemaAccepted,
ajv.errorsText(validate.errors, { separator: "\n" }),
).toBe(expected);
expect(runtimeAccepts(parser, value)).toBe(expected);
}
const validApp = (): Record<string, unknown> => ({
schemaVersion: 1,
id: "de.add-ideas.example",
name: "Example",
version: "1.2.3",
description: "Example application",
entry: "./",
icon: "./icon.svg",
categories: ["documents"],
tags: ["example"],
integration: {
contextVersion: 1,
launchModes: ["navigate"],
embedding: "unsupported",
},
requirements: {
secureContext: false,
workers: false,
indexedDb: false,
crossOriginIsolated: false,
},
privacy: {
processing: "local",
fileUploads: false,
telemetry: false,
},
source: {
repository: "https://example.test/source",
license: "MIT",
},
});
const validCatalog = (): Record<string, unknown> => ({
schemaVersion: 1,
id: "de.add-ideas.toolbox",
name: "Example Toolbox",
home: "./",
theme: { mode: "system", brand: "Example" },
apps: [{ manifest: "./apps/example/toolbox-app.json", enabled: true }],
});
describe("canonical schema and runtime parser parity", () => {
const acceptedApps: Array<[string, () => Record<string, unknown>]> = [
["the baseline app", validApp],
[
"relative references containing spaces and path @ signs",
() => ({
...validApp(),
entry: " ./nested app/?mode=1#result ",
actions: [{ id: "docs", label: "Docs", url: "./people/@me" }],
}),
],
[
"case-insensitive and compact absolute HTTP URLs",
() => ({
...validApp(),
source: { repository: "HTTPS:example.test", license: "MIT" },
}),
],
];
it.each(acceptedApps)("accepts %s", (_label, fixture) => {
expectParity(validateApp, parseToolboxApp, fixture(), true);
});
const rejectedApps: Array<[string, () => Record<string, unknown>]> = [
["a whitespace-only name", () => ({ ...validApp(), name: " \t " })],
[
"a whitespace-only list item",
() => ({ ...validApp(), categories: ["documents", " \n "] }),
],
[
"a non-web entry scheme",
() => ({ ...validApp(), entry: "javascript:alert(1)" }),
],
["a backslash path", () => ({ ...validApp(), icon: ".\\icon.svg" })],
["an empty HTTP authority", () => ({ ...validApp(), entry: "https://" })],
[
"a credential-bearing privacy URL",
() => ({
...validApp(),
privacy: {
processing: "local",
fileUploads: false,
telemetry: false,
url: "https://user:secret@example.test/privacy",
},
}),
],
[
"a relative source repository",
() => ({
...validApp(),
source: { repository: "../source", license: "MIT" },
}),
],
[
"a compact credential-bearing source repository",
() => ({
...validApp(),
source: { repository: "https:user@example.test", license: "MIT" },
}),
],
[
"a whitespace-only action label",
() => ({
...validApp(),
actions: [{ id: "docs", label: " ", url: "./docs" }],
}),
],
];
it.each(rejectedApps)("rejects %s", (_label, fixture) => {
expectParity(validateApp, parseToolboxApp, fixture(), false);
});
const acceptedCatalogs: Array<[string, () => Record<string, unknown>]> = [
["the baseline catalog", validCatalog],
[
"an uppercase HTTPS manifest URL",
() => ({
...validCatalog(),
apps: [
{
manifest: "HTTPS://example.test/apps/example/toolbox-app.json",
enabled: true,
},
],
}),
],
];
it.each(acceptedCatalogs)("accepts %s", (_label, fixture) => {
expectParity(validateCatalog, parseToolboxCatalog, fixture(), true);
});
const rejectedCatalogs: Array<[string, () => Record<string, unknown>]> = [
[
"a whitespace-only catalog name",
() => ({ ...validCatalog(), name: " " }),
],
[
"a whitespace-only brand",
() => ({
...validCatalog(),
theme: { mode: "system", brand: "\t" },
}),
],
[
"a non-web home URL",
() => ({ ...validCatalog(), home: "file:///tmp/toolbox" }),
],
[
"a backslash manifest path",
() => ({
...validCatalog(),
apps: [{ manifest: ".\\apps\\toolbox-app.json", enabled: true }],
}),
],
[
"a credential-bearing manifest URL",
() => ({
...validCatalog(),
apps: [
{
manifest: "https://user:secret@example.test/toolbox-app.json",
enabled: true,
},
],
}),
],
[
"a non-web external entry",
() => ({
...validCatalog(),
apps: [
{
name: "External",
entry: "data:text/html,hello",
launch: "new-tab",
enabled: true,
},
],
}),
],
[
"a whitespace-only external name",
() => ({
...validCatalog(),
apps: [
{
name: " ",
entry: "https://example.test/",
launch: "new-tab",
enabled: true,
},
],
}),
],
];
it.each(rejectedCatalogs)("rejects %s", (_label, fixture) => {
expectParity(validateCatalog, parseToolboxCatalog, fixture(), false);
});
});