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

231
README.md Normal file
View File

@@ -0,0 +1,231 @@
# Toolbox SDK
Toolbox SDK defines a small, versioned contract for independently deployed web
tools. An app always works by itself. When it receives a trusted same-origin
catalog URL, the same app gains a toolbox home link and an app switcher without
becoming coupled to a portal or client-side router.
Version `0.1.0` contains three publish-ready packages:
- `@add-ideas/toolbox-contract` — types, strict v1 runtime parsing, context
discovery/loading, resolved URLs, and contextual link helpers.
- `@add-ideas/toolbox-shell-react``AppShell` plus the v1 CSS theme.
- `@add-ideas/toolbox-testkit` — the `toolbox-check <dist>` build validation and
nested-path smoke-test CLI.
The project is licensed under Apache-2.0. That permissive license was chosen in
particular for its explicit patent grant.
## Install
The repository includes a token-free `.npmrc` pointing the `@add-ideas` scope at
the ADD Ideas package registry. Supply registry credentials through your normal
user/CI npm configuration; never add them to this repository.
```sh
npm install @add-ideas/toolbox-contract @add-ideas/toolbox-shell-react
npm install --save-dev @add-ideas/toolbox-testkit
```
React and ReactDOM are peer dependencies of the shell (`>=18 <20`).
## Application manifest v1
Deploy `toolbox-app.json` beside the application entry. The canonical schema is
[`schemas/toolbox-app.v1.schema.json`](./schemas/toolbox-app.v1.schema.json).
```json
{
"$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
"schemaVersion": 1,
"id": "de.add-ideas.pdf-tools",
"name": "PDF Workbench",
"version": "0.3.4",
"description": "Page-level PDF operations performed locally in the browser.",
"entry": "./",
"icon": "./favicon.svg",
"categories": ["documents", "pdf"],
"tags": ["merge", "split", "rotate"],
"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.add-ideas.de/zemion/pdf-tools",
"license": "AGPL-3.0-only"
}
}
```
`source`, `privacy.label`, `privacy.url`, `requirements.topLevelContext`,
`actions`, and `assets` are optional v1 additions. `toolbox-check` verifies the
entry, icon, and every declared asset. Runtime parsers validate every known
field, require `schemaVersion: 1`, and deliberately discard unknown fields so
future optional additions do not break v1 consumers.
For typed source definitions, use the literal-preserving identity helper:
```ts
import { defineToolboxApp } from "@add-ideas/toolbox-contract";
export const manifest = defineToolboxApp({
// The same fields as toolbox-app.json.
});
```
Use `parseToolboxApp(unknownValue)` at trust boundaries; `defineToolboxApp()` is
compile-time only and does not replace runtime parsing.
## Catalog v1
The canonical schema is
[`schemas/toolbox-catalog.v1.schema.json`](./schemas/toolbox-catalog.v1.schema.json).
A catalog owns its home and theme. Manifest references resolve relative to the
catalog. An external entry may be declared inline when no toolbox manifest is
available.
```json
{
"schemaVersion": 1,
"id": "de.add-ideas.toolbox",
"name": "add·ideas Toolbox",
"home": "./",
"theme": { "mode": "system", "brand": "add·ideas" },
"apps": [
{ "manifest": "./apps/pdf/toolbox-app.json", "enabled": true },
{
"name": "Documentation",
"entry": "https://docs.example.org/",
"launch": "new-tab",
"enabled": true
}
]
}
```
Disabled entries remain in the parsed catalog but are not fetched or exposed in
the resolved app list. `loadToolboxCatalog()` fetches enabled manifests and
returns resolved manifest, entry, icon, asset, action, privacy, home, and
external-entry URLs.
## Context discovery and security
An app discovers context in this order:
1. `?toolbox=/toolbox.catalog.json`
2. `<meta name="toolbox" content="/toolbox.catalog.json">`
3. standalone mode when neither exists
The query parameter takes precedence. Discovered catalog URLs and their manifest
documents must share the current page origin; cross-origin values are rejected
before fetch. Credential-bearing and non-HTTP(S) URLs are rejected. Unknown,
invalid, or unavailable context is returned as a discriminated error, not an
uncaught rejection:
```ts
import { loadToolboxContext } from "@add-ideas/toolbox-contract";
const result = await loadToolboxContext();
if (result.status === "ready") {
console.log(result.context.catalog.apps);
} else if (result.status === "standalone") {
console.log("No toolbox requested");
} else {
console.warn(result.error.code, result.error.message);
}
```
`contextualizeToolboxLink(target, catalog, { location })` (also exported as
`createContextualLink`) adds the context parameter only to same-origin targets.
External targets are resolved but never decorated with the context URL.
## React shell
```tsx
import { AppShell } from "@add-ideas/toolbox-shell-react";
import "@add-ideas/toolbox-shell-react/styles.css";
<AppShell
app={manifest}
manifestUrl="/toolbox-app.json"
appActions={<a href="./help/">Help</a>}
onContextError={(error) => console.warn(error)}
>
<Application />
</AppShell>;
```
The shell always renders app identity as an `h1`, version, derived privacy
facts, manifest actions, and `appActions`. With valid context it additionally
renders the catalog brand/home and a switcher at the accessible navigation
landmark `Toolbox applications`. Destinations are ordinary links, so switching
performs full-page navigation. On missing, cross-origin, invalid, or unavailable
context the shell quietly remains usable in standalone mode; `onContextError` is
optional observability.
CSS custom properties prefixed with `--toolbox-` are the v1 theme surface. The
catalog `light`, `dark`, or `system` mode selects the built-in palette.
## Validate a build
```json
{
"scripts": {
"toolbox:check": "toolbox-check dist"
}
}
```
`toolbox-check` strictly parses `dist/toolbox-app.json`, checks its id and
SemVer version, rejects absolute/traversing/escaping local asset paths, verifies
the entry/icon/assets, serves the build from a deep nested prefix, and
smoke-fetches both standalone and `?toolbox=/toolbox.catalog.json` modes. It
also follows local script, stylesheet, image, icon, and web-manifest references
in the entry HTML (including web-manifest icons), rejecting root-absolute
references that would break a nested deployment. It uses Node fetch and a
temporary in-process HTTP server; browser behavior is covered by the shell's
jsdom tests.
## Contract API
The main exports are:
- Definitions: `ToolboxAppManifest`, `ToolboxCatalog`, their nested and resolved
types, `defineToolboxApp()`, and `defineToolboxCatalog()`.
- Validation: `parseToolboxApp()`, `parseToolboxCatalog()`,
`ToolboxValidationError`, and `ToolboxError`.
- Discovery/loading: `discoverToolboxCatalog()`, `fetchToolboxAppManifest()`,
`fetchToolboxCatalog()`, `loadToolboxCatalog()`, and `loadToolboxContext()`.
- URLs: `resolveWebUrl()`, `resolveToolboxApp()`, `requireSameOrigin()`, and
`contextualizeToolboxLink()`.
See each emitted `.d.ts` file for the complete signatures.
## Develop
Requires Node 20 or newer.
```sh
npm install
npm run check
```
`npm run check` runs strict TypeScript checks, ESLint, Prettier verification,
Vitest/jsdom tests, and all package builds. The workspace uses current versions
aligned with the consuming apps: React 19.2, TypeScript 6, Vitest 4, and
ESLint 10.
Package publication is intentionally separate from the build. Authenticate in
the caller environment, then use npm workspace publishing commands after review.