# 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.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 ` 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. `` 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"; setHelpOpen(true) }} onContextError={(error) => console.warn(error)} > ; ``` The shell always renders a centered app identity, version, derived privacy facts, standard Help and source controls, Personalize, Apps, manifest actions, and legacy `appActions`. With valid context it additionally renders the catalog brand/home 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. 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.