Files
toolbox-sdk/README.md
T
zemion bc91659a42
Verify / verify (push) Canceled after 0s
Release Toolbox SDK 0.3.0
2026-09-02 00:58:50 +02:00

287 lines
11 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.3.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/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
},
"io": {
"accepts": [{ "mediaType": "application/pdf", "extensions": [".pdf"] }],
"produces": [{ "mediaType": "application/pdf", "extensions": [".pdf"] }]
},
"capabilities": {
"required": ["workers"],
"optional": ["file-system-access"]
},
"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`, `io`,
`capabilities`, `actions`, and `assets` are optional v1 additions. `io`
advertises accepted and produced media types/extensions; `capabilities`
describes required and progressive browser features. `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.
When a manifest includes `capabilities`, worker declarations are cross-checked:
`requirements.workers: true` means the app cannot run without workers and
therefore requires `"workers"` in `capabilities.required`. Apps with a
main-thread fallback set the requirement to `false` and may list `"workers"` in
`capabilities.optional` instead. Legacy v1 manifests without a capability
profile remain valid.
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.
## Explicit local artifact handoff
`createToolboxTransfer()` stores bounded `Blob` objects in same-origin IndexedDB
using a cryptographic, short-lived token routed to one target app. Only that
opaque token is added to the target URL by `createToolboxTransferUrl()`. The
target calls `consumeToolboxTransfer()` and the record is atomically deleted.
File bytes are neither uploaded nor placed in URLs, cross-origin destinations
are rejected, and transfers expire after fifteen minutes by default. File
descriptors, evidence, counts, names, sizes, and lifetimes are bounded before
IndexedDB receives them.
The target app id is a routing/integrity check in this API, not a browser access
control boundary. IndexedDB is shared by the entire origin, so any script
running on that origin can open the transfer database directly and read or
delete its records. Deploy only mutually trusted, reviewed Toolbox apps on one
origin; an untrusted app must use a separate origin and cannot participate in
this same-origin handoff.
## 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.