Files
toolbox-sdk/README.md

7.7 KiB

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-reactAppShell 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.

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.

{
  "$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:

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. 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.

{
  "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:

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

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

{
  "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.

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.