252 lines
9.0 KiB
Markdown
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.
|