Files
toolbox-sdk/README.md

252 lines
9.0 KiB
Markdown

# 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.2.3` 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/lotobo/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/lotobo/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"
helpAction={{ onClick: () => setHelpOpen(true) }}
onContextError={(error) => console.warn(error)}
>
<Application />
</AppShell>;
```
The shell's fixed top row renders the Toolbox brand/home at the far left, a
geometrically centered text identity, and Help, source, Apps, and Personalize
controls at the far right in that order. Personalize and Apps have labeled
desktop controls and retain their accessible names when compact. The mutually
exclusive popovers close on Escape, outside interaction, and link selection.
`AppShell` moves version, derived privacy facts, manifest actions, and legacy
`appActions` into its standard footer, keeping the top row stable in connected
and standalone modes. With valid context it supplies the catalog home, favicon,
and available app destinations. 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.
For a custom portal-style header, `ToolboxHeader` accepts `personalizeContent`
for content hosted inside the shared controlled popover and `brandIconUrl` for
the Toolbox mark. `ToolboxPersonalizePanel` provides the shared portal-style
heading, storage warning, and appearance picker, with a child slot for
portal-specific controls. `onPersonalize` remains available for callback-only
integrations. The deprecated `iconUrl`, `leadingActions`, and `metadata` props
are retained for source compatibility; the centered app icon is intentionally no
longer rendered.
CSS custom properties prefixed with `--toolbox-` are the v1 theme surface. The
user's `light`, `dark`, or `system` preference is shared with same-origin apps
under `TOOLBOX_PREFERENCES_KEY`; the catalog mode is used until a preference is
saved. `ToolboxHeader` is also exported for portal-like pages that do not need
the complete application shell.
## 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()`.
- Preferences: `parseToolboxPreferences()`, `readToolboxPreferences()`,
`writeToolboxPreferences()`, and `TOOLBOX_PREFERENCES_KEY`.
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.