feat: add toolbox contract shell and testkit
This commit is contained in:
231
README.md
Normal file
231
README.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 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-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/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. `<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"
|
||||
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
|
||||
|
||||
```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()`.
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user