commit ce9bddb218110dd6c13c106668585fe68df4800e Author: Albrecht Degering Date: Mon Jul 20 17:53:56 2026 +0200 feat: add toolbox contract shell and testkit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a3a638 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +coverage/ +*.tsbuildinfo +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..90e6041 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@add-ideas:registry=https://git.add-ideas.de/api/packages/zemion/npm/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..012fbaa --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +dist +coverage +node_modules +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..05a170c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "proseWrap": "always", + "trailingComma": "all" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..717c4cb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to the independently versioned Toolbox SDK packages are +recorded here. The packages currently share one release version. + +## 0.1.0 — 2026-07-20 + +### Added + +- `@add-ideas/toolbox-contract` with v1 app and catalog types, published JSON + Schemas, strict forward-compatible runtime parsing, same-origin context + discovery, hydrated catalog loading, resolved URL helpers, and graceful result + types. +- `@add-ideas/toolbox-shell-react` with the standalone-safe `AppShell`, built-in + light/dark/system theme, app metadata and privacy display, actions, toolbox + home, and accessible full-page app switching. +- `@add-ideas/toolbox-testkit` with the `toolbox-check` CLI for manifest/assets, + unsafe-path detection, nested-prefix resource checks, and standalone/context + HTTP smoke tests. +- React integration and manifest/catalog examples, Vitest/jsdom coverage, and + workspace build, typecheck, lint, formatting, and package scripts. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c38585f --- /dev/null +++ b/LICENSE @@ -0,0 +1,192 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + Copyright 2026 ADD Ideas + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cd6011 --- /dev/null +++ b/README.md @@ -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 ` 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"; + +Help} + onContextError={(error) => console.warn(error)} +> + +; +``` + +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. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..aa91ef4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,31 @@ +import eslint from "@eslint/js"; +import reactHooks from "eslint-plugin-react-hooks"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["**/dist/**", "**/coverage/**", "**/node_modules/**"], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + "@typescript-eslint/consistent-type-imports": "error", + }, + }, + { + files: ["**/*.tsx"], + plugins: { + "react-hooks": reactHooks, + }, + rules: reactHooks.configs.recommended.rules, + }, +); diff --git a/examples/react-app/README.md b/examples/react-app/README.md new file mode 100644 index 0000000..a2d66f8 --- /dev/null +++ b/examples/react-app/README.md @@ -0,0 +1,15 @@ +# React integration example + +The source files show the intended integration points for an existing React +application. Copy `public/toolbox-app.json` into the build unchanged (or inject +the package version during the build), import the shell CSS once, and wrap the +application in `AppShell`. + +In the built application, run: + +```sh +toolbox-check dist +``` + +The adjacent example catalog is illustrative. A production catalog normally +lives at the toolbox home and points to each deployed app manifest. diff --git a/examples/react-app/public/icon.svg b/examples/react-app/public/icon.svg new file mode 100644 index 0000000..ec19e3e --- /dev/null +++ b/examples/react-app/public/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/react-app/public/toolbox-app.json b/examples/react-app/public/toolbox-app.json new file mode 100644 index 0000000..5b5f5ae --- /dev/null +++ b/examples/react-app/public/toolbox-app.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json", + "schemaVersion": 1, + "id": "de.add-ideas.example-tool", + "name": "Example Tool", + "version": "0.1.0", + "description": "A minimal toolbox-compatible React application.", + "entry": "./", + "icon": "./icon.svg", + "categories": ["examples"], + "tags": ["react", "toolbox"], + "integration": { + "contextVersion": 1, + "launchModes": ["navigate", "new-tab"], + "embedding": "unsupported" + }, + "requirements": { + "secureContext": false, + "workers": false, + "indexedDb": false, + "crossOriginIsolated": false + }, + "privacy": { + "processing": "local", + "fileUploads": false, + "telemetry": false + }, + "source": { + "repository": "https://git.add-ideas.de/zemion/toolbox-sdk", + "license": "Apache-2.0" + } +} diff --git a/examples/react-app/public/toolbox.catalog.json b/examples/react-app/public/toolbox.catalog.json new file mode 100644 index 0000000..2fc2332 --- /dev/null +++ b/examples/react-app/public/toolbox.catalog.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json", + "schemaVersion": 1, + "id": "de.add-ideas.example-toolbox", + "name": "Example Toolbox", + "home": "./", + "theme": { + "mode": "system", + "brand": "Example Toolbox" + }, + "apps": [ + { + "manifest": "./toolbox-app.json", + "enabled": true + }, + { + "name": "ADD Ideas", + "entry": "https://add-ideas.de/", + "launch": "new-tab", + "enabled": true + } + ] +} diff --git a/examples/react-app/src/App.tsx b/examples/react-app/src/App.tsx new file mode 100644 index 0000000..2c54edc --- /dev/null +++ b/examples/react-app/src/App.tsx @@ -0,0 +1,19 @@ +import { AppShell } from "@add-ideas/toolbox-shell-react"; +import "@add-ideas/toolbox-shell-react/styles.css"; + +import { toolboxApp } from "./toolbox-app.js"; + +export function App() { + return ( + Help}> +
+

Ready

+

+ Open this app directly for standalone mode, or add + ?toolbox=/toolbox.catalog.json to connect it to a + same-origin catalog. +

+
+
+ ); +} diff --git a/examples/react-app/src/main.tsx b/examples/react-app/src/main.tsx new file mode 100644 index 0000000..e6f85dc --- /dev/null +++ b/examples/react-app/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import { App } from "./App.js"; + +const root = document.querySelector("#root"); +if (!root) throw new Error("Missing #root element"); + +createRoot(root).render( + + + , +); diff --git a/examples/react-app/src/toolbox-app.ts b/examples/react-app/src/toolbox-app.ts new file mode 100644 index 0000000..668dfa0 --- /dev/null +++ b/examples/react-app/src/toolbox-app.ts @@ -0,0 +1,33 @@ +import { defineToolboxApp } from "@add-ideas/toolbox-contract"; + +export const toolboxApp = defineToolboxApp({ + schemaVersion: 1, + id: "de.add-ideas.example-tool", + name: "Example Tool", + version: "0.1.0", + description: "A minimal toolbox-compatible React application.", + entry: "./", + icon: "./icon.svg", + categories: ["examples"], + tags: ["react", "toolbox"], + integration: { + contextVersion: 1, + launchModes: ["navigate", "new-tab"], + embedding: "unsupported", + }, + requirements: { + secureContext: false, + workers: false, + indexedDb: false, + crossOriginIsolated: false, + }, + privacy: { + processing: "local", + fileUploads: false, + telemetry: false, + }, + source: { + repository: "https://git.add-ideas.de/zemion/toolbox-sdk", + license: "Apache-2.0", + }, +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0b24d67 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3982 @@ +{ + "name": "@add-ideas/toolbox-sdk-workspace", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@add-ideas/toolbox-sdk-workspace", + "version": "0.1.0", + "license": "Apache-2.0", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^25.8.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "ajv": "^8.20.0", + "eslint": "^10.4.0", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "prettier": "^3.8.3", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3", + "vitest": "^4.1.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@add-ideas/toolbox-contract": { + "resolved": "packages/contract", + "link": true + }, + "node_modules/@add-ideas/toolbox-shell-react": { + "resolved": "packages/shell-react", + "link": true + }, + "node_modules/@add-ideas/toolbox-testkit": { + "resolved": "packages/testkit", + "link": true + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.64.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.393", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz", + "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz", + "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.64.0", + "@typescript-eslint/parser": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "packages/contract": { + "name": "@add-ideas/toolbox-contract", + "version": "0.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + } + }, + "packages/shell-react": { + "name": "@add-ideas/toolbox-shell-react", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@add-ideas/toolbox-contract": "0.1.0" + }, + "peerDependencies": { + "react": ">=18 <20", + "react-dom": ">=18 <20" + } + }, + "packages/testkit": { + "name": "@add-ideas/toolbox-testkit", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@add-ideas/toolbox-contract": "0.1.0" + }, + "bin": { + "toolbox-check": "dist/cli.js" + }, + "engines": { + "node": ">=20" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..287753b --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "@add-ideas/toolbox-sdk-workspace", + "version": "0.1.0", + "private": true, + "description": "A small, framework-neutral toolbox contract with a React application shell.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://git.add-ideas.de/zemion/toolbox-sdk.git" + }, + "type": "module", + "workspaces": [ + "packages/*" + ], + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "npm run build -w @add-ideas/toolbox-contract && npm run build -w @add-ideas/toolbox-shell-react && npm run build -w @add-ideas/toolbox-testkit", + "clean": "npm run clean --workspaces --if-present", + "typecheck": "npm run typecheck --workspaces --if-present", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint . --max-warnings=0", + "format": "prettier --check .", + "format:write": "prettier --write .", + "check": "npm run typecheck && npm run lint && npm run format && npm test && npm run build" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^25.8.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "ajv": "^8.20.0", + "eslint": "^10.4.0", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "prettier": "^3.8.3", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3", + "vitest": "^4.1.6" + }, + "packageManager": "npm@11.17.0" +} diff --git a/packages/contract/LICENSE b/packages/contract/LICENSE new file mode 100644 index 0000000..c38585f --- /dev/null +++ b/packages/contract/LICENSE @@ -0,0 +1,192 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + Copyright 2026 ADD Ideas + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/contract/README.md b/packages/contract/README.md new file mode 100644 index 0000000..9e39c93 --- /dev/null +++ b/packages/contract/README.md @@ -0,0 +1,21 @@ +# @add-ideas/toolbox-contract + +Typed, runtime-validated toolbox application manifests and catalogs, plus safe +same-origin context discovery and URL resolution. + +```ts +import { + loadToolboxContext, + parseToolboxApp, +} from "@add-ideas/toolbox-contract"; + +const app = parseToolboxApp(await response.json()); +const result = await loadToolboxContext(); +``` + +See the workspace [README](https://git.add-ideas.de/zemion/toolbox-sdk#readme) +for the v1 document formats and full API. + +The canonical schemas are also published at +`@add-ideas/toolbox-contract/schemas/toolbox-app.v1.schema.json` and +`@add-ideas/toolbox-contract/schemas/toolbox-catalog.v1.schema.json`. diff --git a/packages/contract/package.json b/packages/contract/package.json new file mode 100644 index 0000000..02b3bb5 --- /dev/null +++ b/packages/contract/package.json @@ -0,0 +1,48 @@ +{ + "name": "@add-ideas/toolbox-contract", + "version": "0.1.0", + "description": "Runtime-validated manifests, catalogs, discovery, and URL helpers for toolbox applications.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://git.add-ideas.de/zemion/toolbox-sdk.git", + "directory": "packages/contract" + }, + "homepage": "https://git.add-ideas.de/zemion/toolbox-sdk", + "keywords": [ + "toolbox", + "manifest", + "catalog", + "typescript" + ], + "type": "module", + "sideEffects": false, + "engines": { + "node": ">=20" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./schemas/toolbox-app.v1.schema.json": "./dist/schemas/toolbox-app.v1.schema.json", + "./schemas/toolbox-catalog.v1.schema.json": "./dist/schemas/toolbox-catalog.v1.schema.json", + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');fs.mkdirSync('dist/schemas',{recursive:true});for(const name of ['toolbox-app.v1.schema.json','toolbox-catalog.v1.schema.json'])fs.copyFileSync('../../schemas/'+name,'dist/schemas/'+name);fs.copyFileSync('../../LICENSE','LICENSE')\"", + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "prepack": "npm run build", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "publishConfig": { + "access": "public", + "registry": "https://git.add-ideas.de/api/packages/zemion/npm/" + } +} diff --git a/packages/contract/src/discovery.ts b/packages/contract/src/discovery.ts new file mode 100644 index 0000000..6fafe13 --- /dev/null +++ b/packages/contract/src/discovery.ts @@ -0,0 +1,110 @@ +import { + TOOLBOX_META_NAME, + TOOLBOX_QUERY_PARAMETER, + ToolboxError, + type ToolboxDiscovery, +} from "./types.js"; +import { requireSameOrigin, resolveWebUrl } from "./urls.js"; + +interface MetaElementLike { + getAttribute(name: string): string | null; +} + +interface DocumentLike { + querySelector(selector: string): MetaElementLike | null; +} + +export interface ToolboxDiscoveryOptions { + location?: string | URL; + document?: DocumentLike; + queryParameter?: string; + metaName?: string; +} + +function browserLocation(): string | undefined { + return typeof globalThis.location === "undefined" + ? undefined + : globalThis.location.href; +} + +function browserDocument(): DocumentLike | undefined { + return typeof globalThis.document === "undefined" + ? undefined + : globalThis.document; +} + +export function discoverToolboxCatalog( + options: ToolboxDiscoveryOptions = {}, +): ToolboxDiscovery { + const locationReference = options.location ?? browserLocation(); + if (locationReference === undefined) return { status: "none" }; + + let location: URL; + try { + location = resolveWebUrl(locationReference); + } catch (error) { + return { + status: "error", + error: + error instanceof ToolboxError + ? error + : new ToolboxError("invalid-url", "The page URL is invalid", { + cause: error, + }), + }; + } + + const parameter = options.queryParameter ?? TOOLBOX_QUERY_PARAMETER; + const queryValue = location.searchParams.get(parameter); + const document = options.document ?? browserDocument(); + let metaValue: string | null | undefined; + try { + const metaName = (options.metaName ?? TOOLBOX_META_NAME) + .replaceAll("\\", "\\\\") + .replaceAll('"', '\\"'); + metaValue = document + ?.querySelector(`meta[name="${metaName}"]`) + ?.getAttribute("content"); + } catch (error) { + return { + status: "error", + error: new ToolboxError("invalid-url", "Toolbox meta discovery failed", { + cause: error, + }), + }; + } + const candidate = queryValue ?? metaValue; + if (candidate === null || candidate === undefined) return { status: "none" }; + if (candidate.trim().length === 0) { + return { + status: "error", + error: new ToolboxError( + "invalid-url", + `The ${queryValue === null ? "meta" : "query"} toolbox URL is empty`, + ), + }; + } + + try { + const catalogUrl = requireSameOrigin( + resolveWebUrl(candidate, location), + location, + "Discovered toolbox catalog", + ); + return { + status: "found", + source: queryValue === null ? "meta" : "query", + catalogUrl, + }; + } catch (error) { + return { + status: "error", + error: + error instanceof ToolboxError + ? error + : new ToolboxError("invalid-url", "The toolbox URL is invalid", { + cause: error, + }), + }; + } +} diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts new file mode 100644 index 0000000..2cdf448 --- /dev/null +++ b/packages/contract/src/index.ts @@ -0,0 +1,60 @@ +export { + TOOLBOX_META_NAME, + TOOLBOX_QUERY_PARAMETER, + TOOLBOX_SCHEMA_VERSION, + ToolboxError, + ToolboxValidationError, +} from "./types.js"; +export type { + ResolvedToolboxAction, + ResolvedToolboxApp, + ResolvedToolboxCatalog, + ResolvedToolboxCatalogEntry, + ResolvedToolboxCatalogExternalEntry, + ResolvedToolboxCatalogManifestEntry, + ResolvedToolboxPrivacy, + ToolboxAction, + ToolboxAppManifest, + ToolboxCatalog, + ToolboxCatalogEntry, + ToolboxCatalogExternalEntry, + ToolboxCatalogManifestEntry, + ToolboxCatalogTheme, + ToolboxContext, + ToolboxContextResult, + ToolboxDiscovery, + ToolboxDiscoverySource, + ToolboxErrorCode, + ToolboxIntegration, + ToolboxLaunchMode, + ToolboxPrivacy, + ToolboxRequirements, + ToolboxSource, +} from "./types.js"; +export { + createContextualLink, + contextualizeToolboxLink, + requireSameOrigin, + resolveToolboxApp, + resolveWebUrl, +} from "./urls.js"; +export type { ContextualLinkOptions } from "./urls.js"; +export { + defineToolboxApp, + defineToolboxCatalog, + parseToolboxApp, + parseToolboxCatalog, +} from "./parse.js"; +export { discoverToolboxCatalog } from "./discovery.js"; +export type { ToolboxDiscoveryOptions } from "./discovery.js"; +export { + fetchToolboxAppManifest, + fetchToolboxCatalog, + loadToolboxCatalog, + loadToolboxContext, +} from "./load.js"; +export type { + ToolboxContextLoadOptions, + ToolboxFetch, + ToolboxLoadOptions, +} from "./load.js"; diff --git a/packages/contract/src/load.ts b/packages/contract/src/load.ts new file mode 100644 index 0000000..9bf4bcf --- /dev/null +++ b/packages/contract/src/load.ts @@ -0,0 +1,245 @@ +import { + discoverToolboxCatalog, + type ToolboxDiscoveryOptions, +} from "./discovery.js"; +import { parseToolboxApp, parseToolboxCatalog } from "./parse.js"; +import { + ToolboxError, + type ResolvedToolboxCatalog, + type ToolboxAppManifest, + type ToolboxCatalog, + type ToolboxContextResult, + type ToolboxDiscoverySource, +} from "./types.js"; +import { requireSameOrigin, resolveToolboxApp, resolveWebUrl } from "./urls.js"; + +export type ToolboxFetch = ( + input: string | URL | Request, + init?: RequestInit, +) => Promise; + +export interface ToolboxLoadOptions { + fetch?: ToolboxFetch; + signal?: AbortSignal; + baseUrl?: string | URL; + expectedOrigin?: string | URL; +} + +function defaultFetch(): ToolboxFetch { + if (typeof globalThis.fetch !== "function") { + throw new ToolboxError( + "fetch-failed", + "No fetch implementation is available", + ); + } + return globalThis.fetch.bind(globalThis); +} + +function absoluteUrl(reference: string | URL, base?: string | URL): URL { + if (base !== undefined) return resolveWebUrl(reference, base); + if (typeof reference !== "string" || /^[a-z][a-z\d+.-]*:/i.test(reference)) { + return resolveWebUrl(reference); + } + if (typeof globalThis.location !== "undefined") { + return resolveWebUrl(reference, globalThis.location.href); + } + throw new ToolboxError( + "invalid-url", + `A base URL is required for relative URL: ${reference}`, + ); +} + +async function fetchJson( + url: URL, + options: ToolboxLoadOptions, +): Promise { + let response: Response; + try { + response = await (options.fetch ?? defaultFetch())(url, { + headers: { accept: "application/json" }, + redirect: "error", + ...(options.signal === undefined ? {} : { signal: options.signal }), + }); + } catch (cause) { + throw new ToolboxError("fetch-failed", `Could not fetch ${url.href}`, { + cause, + }); + } + if (typeof response.url === "string" && response.url !== "") { + requireSameOrigin( + resolveWebUrl(response.url), + url, + `Final response for ${url.href}`, + ); + } + if (!response.ok) { + throw new ToolboxError( + "fetch-failed", + `Could not fetch ${url.href}: HTTP ${response.status}`, + ); + } + try { + return await response.json(); + } catch (cause) { + throw new ToolboxError("fetch-failed", `${url.href} is not valid JSON`, { + cause, + }); + } +} + +export async function fetchToolboxAppManifest( + manifestUrl: string | URL, + options: ToolboxLoadOptions = {}, +): Promise { + const url = absoluteUrl(manifestUrl, options.baseUrl); + if (options.expectedOrigin !== undefined) { + requireSameOrigin( + url, + absoluteUrl(options.expectedOrigin), + "Application manifest", + ); + } + return parseToolboxApp(await fetchJson(url, options)); +} + +export async function fetchToolboxCatalog( + catalogUrl: string | URL, + options: ToolboxLoadOptions = {}, +): Promise { + const url = absoluteUrl(catalogUrl, options.baseUrl); + if (options.expectedOrigin !== undefined) { + requireSameOrigin( + url, + absoluteUrl(options.expectedOrigin), + "Toolbox catalog", + ); + } + return parseToolboxCatalog(await fetchJson(url, options)); +} + +export async function loadToolboxCatalog( + catalogReference: string | URL, + options: ToolboxLoadOptions = {}, +): Promise { + const catalogUrl = absoluteUrl(catalogReference, options.baseUrl); + const expectedOrigin = + options.expectedOrigin === undefined + ? catalogUrl + : absoluteUrl(options.expectedOrigin); + requireSameOrigin(catalogUrl, expectedOrigin, "Toolbox catalog"); + + const catalog = await fetchToolboxCatalog(catalogUrl, { + ...options, + expectedOrigin, + }); + const apps = await Promise.all( + catalog.apps + .filter((entry) => entry.enabled) + .map(async (entry) => { + if ("entry" in entry) { + return { + kind: "external" as const, + enabled: entry.enabled, + name: entry.name, + entryUrl: resolveWebUrl(entry.entry, catalogUrl), + launch: entry.launch, + }; + } + const manifestUrl = requireSameOrigin( + resolveWebUrl(entry.manifest, catalogUrl), + expectedOrigin, + "Catalog application manifest", + ); + const manifest = await fetchToolboxAppManifest(manifestUrl, { + ...options, + expectedOrigin, + }); + return { + kind: "manifest" as const, + enabled: entry.enabled, + app: resolveToolboxApp(manifest, manifestUrl, expectedOrigin), + }; + }), + ); + const homeUrl = requireSameOrigin( + resolveWebUrl(catalog.home, catalogUrl), + expectedOrigin, + "Toolbox home", + ); + + return { + catalog, + catalogUrl, + apps, + homeUrl, + }; +} + +export interface ToolboxContextLoadOptions + extends + ToolboxDiscoveryOptions, + Omit { + catalogUrl?: string | URL; +} + +function asToolboxError(error: unknown): ToolboxError { + return error instanceof ToolboxError + ? error + : new ToolboxError( + "fetch-failed", + "The toolbox context could not be loaded", + { + cause: error, + }, + ); +} + +export async function loadToolboxContext( + options: ToolboxContextLoadOptions = {}, +): Promise { + let source: ToolboxDiscoverySource; + let catalogUrl: URL; + let pageUrl: URL; + + if (options.catalogUrl !== undefined) { + source = "explicit"; + try { + pageUrl = resolveWebUrl( + options.location ?? + (typeof globalThis.location === "undefined" + ? options.catalogUrl + : globalThis.location.href), + ); + catalogUrl = requireSameOrigin( + resolveWebUrl(options.catalogUrl, pageUrl), + pageUrl, + "Toolbox catalog", + ); + } catch (error) { + return { status: "error", error: asToolboxError(error) }; + } + } else { + const discovery = discoverToolboxCatalog(options); + if (discovery.status === "none") return { status: "standalone" }; + if (discovery.status === "error") return discovery; + source = discovery.source; + catalogUrl = discovery.catalogUrl; + pageUrl = resolveWebUrl( + options.location ?? + (typeof globalThis.location === "undefined" + ? catalogUrl + : globalThis.location.href), + ); + } + + try { + const catalog = await loadToolboxCatalog(catalogUrl, { + ...(options.fetch === undefined ? {} : { fetch: options.fetch }), + ...(options.signal === undefined ? {} : { signal: options.signal }), + expectedOrigin: pageUrl, + }); + return { status: "ready", context: { source, catalog } }; + } catch (error) { + return { status: "error", error: asToolboxError(error) }; + } +} diff --git a/packages/contract/src/parse.ts b/packages/contract/src/parse.ts new file mode 100644 index 0000000..40d2241 --- /dev/null +++ b/packages/contract/src/parse.ts @@ -0,0 +1,528 @@ +import { + TOOLBOX_SCHEMA_VERSION, + ToolboxValidationError, + type ToolboxAction, + type ToolboxAppManifest, + type ToolboxCatalog, + type ToolboxCatalogEntry, + type ToolboxCatalogExternalEntry, + type ToolboxCatalogManifestEntry, + type ToolboxCatalogTheme, + type ToolboxIntegration, + type ToolboxLaunchMode, + type ToolboxPrivacy, + type ToolboxRequirements, + type ToolboxSource, +} from "./types.js"; + +type UnknownRecord = Record; + +const ID_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/; +const WEB_PROTOCOLS = new Set(["http:", "https:"]); +const REFERENCE_BASE = "https://toolbox.invalid/"; + +function recordAt( + value: unknown, + path: string, + issues: string[], +): UnknownRecord { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + issues.push(`${path} must be an object`); + return {}; + } + return value as UnknownRecord; +} + +function stringAt( + object: UnknownRecord, + key: string, + path: string, + issues: string[], + options: { optional?: boolean; allowEmpty?: boolean } = {}, +): string | undefined { + if (!(key in object) && options.optional) return undefined; + const value = object[key]; + if (typeof value !== "string") { + issues.push(`${path}.${key} must be a string`); + return undefined; + } + if (!options.allowEmpty && value.trim().length === 0) { + issues.push(`${path}.${key} must not be empty`); + return undefined; + } + return value; +} + +function booleanAt( + object: UnknownRecord, + key: string, + path: string, + issues: string[], + optional = false, +): boolean | undefined { + if (!(key in object) && optional) return undefined; + const value = object[key]; + if (typeof value !== "boolean") { + issues.push(`${path}.${key} must be a boolean`); + return undefined; + } + return value; +} + +function enumAt( + object: UnknownRecord, + key: string, + allowed: readonly T[], + path: string, + issues: string[], +): T | undefined { + const value = stringAt(object, key, path, issues); + if (value === undefined) return undefined; + if (!allowed.includes(value as T)) { + issues.push(`${path}.${key} must be one of ${allowed.join(", ")}`); + return undefined; + } + return value as T; +} + +function idAt( + object: UnknownRecord, + key: string, + path: string, + issues: string[], +): string | undefined { + const value = stringAt(object, key, path, issues); + if (value !== undefined && !ID_PATTERN.test(value)) { + issues.push(`${path}.${key} must be a lowercase toolbox id`); + return undefined; + } + return value; +} + +function referenceAt( + object: UnknownRecord, + key: string, + path: string, + issues: string[], + optional = false, +): string | undefined { + const value = stringAt(object, key, path, issues, { optional }); + if (value === undefined) return undefined; + if (value.includes("\\")) { + issues.push(`${path}.${key} must use URL path separators`); + return undefined; + } + try { + const url = new URL(value, REFERENCE_BASE); + if (!WEB_PROTOCOLS.has(url.protocol) || url.username || url.password) { + issues.push(`${path}.${key} must be an HTTP(S) URL reference`); + return undefined; + } + } catch { + issues.push(`${path}.${key} must be a valid URL reference`); + return undefined; + } + return value; +} + +function absoluteReferenceAt( + object: UnknownRecord, + key: string, + path: string, + issues: string[], +): string | undefined { + const value = referenceAt(object, key, path, issues); + if (value === undefined) return undefined; + try { + const url = new URL(value); + if (!WEB_PROTOCOLS.has(url.protocol) || url.username || url.password) { + throw new Error("Unsupported URL"); + } + } catch { + issues.push(`${path}.${key} must be an absolute HTTP(S) URL`); + return undefined; + } + return value; +} + +function schemaVersionAt( + object: UnknownRecord, + path: string, + issues: string[], +): void { + if (object.schemaVersion !== TOOLBOX_SCHEMA_VERSION) { + issues.push(`${path}.schemaVersion must be ${TOOLBOX_SCHEMA_VERSION}`); + } +} + +function stringListAt( + object: UnknownRecord, + key: string, + path: string, + issues: string[], +): readonly string[] { + const value = object[key]; + if (!Array.isArray(value)) { + issues.push(`${path}.${key} must be an array`); + return []; + } + const seen = new Set(); + return value.flatMap((item, index) => { + if (typeof item !== "string" || item.trim().length === 0) { + issues.push(`${path}.${key}[${index}] must be a non-empty string`); + return []; + } + if (seen.has(item)) { + issues.push(`${path}.${key}[${index}] must be unique`); + return []; + } + seen.add(item); + return [item]; + }); +} + +function parsePrivacy( + value: unknown, + path: string, + issues: string[], +): ToolboxPrivacy | undefined { + const object = recordAt(value, path, issues); + const processing = enumAt( + object, + "processing", + ["local", "remote", "mixed"] as const, + path, + issues, + ); + const fileUploads = booleanAt(object, "fileUploads", path, issues); + const telemetry = booleanAt(object, "telemetry", path, issues); + const label = stringAt(object, "label", path, issues, { optional: true }); + const url = referenceAt(object, "url", path, issues, true); + if ( + processing === undefined || + fileUploads === undefined || + telemetry === undefined + ) { + return undefined; + } + return { + processing, + fileUploads, + telemetry, + ...(label === undefined ? {} : { label }), + ...(url === undefined ? {} : { url }), + }; +} + +function parseIntegration( + value: unknown, + path: string, + issues: string[], +): ToolboxIntegration | undefined { + const object = recordAt(value, path, issues); + if (object.contextVersion !== 1) { + issues.push(`${path}.contextVersion must be 1`); + } + const launchModes = object.launchModes; + const modes: ToolboxLaunchMode[] = []; + if (!Array.isArray(launchModes) || launchModes.length === 0) { + issues.push(`${path}.launchModes must be a non-empty array`); + } else { + const seen = new Set(); + launchModes.forEach((mode, index) => { + if (mode !== "navigate" && mode !== "new-tab") { + issues.push(`${path}.launchModes[${index}] is invalid`); + } else if (seen.has(mode)) { + issues.push(`${path}.launchModes[${index}] must be unique`); + } else { + seen.add(mode); + modes.push(mode); + } + }); + } + const embedding = enumAt( + object, + "embedding", + ["unsupported", "supported"] as const, + path, + issues, + ); + return object.contextVersion === 1 && + modes.length > 0 && + embedding !== undefined + ? { contextVersion: 1, launchModes: modes, embedding } + : undefined; +} + +function parseRequirements( + value: unknown, + path: string, + issues: string[], +): ToolboxRequirements | undefined { + const object = recordAt(value, path, issues); + const secureContext = booleanAt(object, "secureContext", path, issues); + const workers = booleanAt(object, "workers", path, issues); + const indexedDb = booleanAt(object, "indexedDb", path, issues); + const crossOriginIsolated = booleanAt( + object, + "crossOriginIsolated", + path, + issues, + ); + const topLevelContext = booleanAt( + object, + "topLevelContext", + path, + issues, + true, + ); + if ( + secureContext === undefined || + workers === undefined || + indexedDb === undefined || + crossOriginIsolated === undefined + ) { + return undefined; + } + return { + secureContext, + workers, + indexedDb, + crossOriginIsolated, + ...(topLevelContext === undefined ? {} : { topLevelContext }), + }; +} + +function parseSource( + value: unknown, + path: string, + issues: string[], +): ToolboxSource | undefined { + const object = recordAt(value, path, issues); + const repository = absoluteReferenceAt(object, "repository", path, issues); + const license = stringAt(object, "license", path, issues); + return repository === undefined || license === undefined + ? undefined + : { repository, license }; +} + +function parseActions( + value: unknown, + path: string, + issues: string[], +): readonly ToolboxAction[] { + if (!Array.isArray(value)) { + issues.push(`${path} must be an array`); + return []; + } + const ids = new Set(); + return value.flatMap((item, index) => { + const itemPath = `${path}[${index}]`; + const object = recordAt(item, itemPath, issues); + const id = idAt(object, "id", itemPath, issues); + const label = stringAt(object, "label", itemPath, issues); + const url = referenceAt(object, "url", itemPath, issues); + if (id !== undefined) { + if (ids.has(id)) issues.push(`${itemPath}.id must be unique`); + ids.add(id); + } + return id === undefined || label === undefined || url === undefined + ? [] + : [{ id, label, url }]; + }); +} + +function parseAssets( + value: unknown, + path: string, + issues: string[], +): readonly string[] { + if (!Array.isArray(value)) { + issues.push(`${path} must be an array`); + return []; + } + const seen = new Set(); + return value.flatMap((item, index) => { + const holder = { value: item }; + const reference = referenceAt(holder, "value", `${path}[${index}]`, issues); + if (reference === undefined) return []; + if (seen.has(reference)) { + issues.push(`${path}[${index}] must be unique`); + return []; + } + seen.add(reference); + return [reference]; + }); +} + +export function parseToolboxApp(value: unknown): ToolboxAppManifest { + const issues: string[] = []; + const object = recordAt(value, "$", issues); + schemaVersionAt(object, "$", issues); + const id = idAt(object, "id", "$", issues); + const name = stringAt(object, "name", "$", issues); + const version = stringAt(object, "version", "$", issues); + const description = stringAt(object, "description", "$", issues, { + allowEmpty: true, + }); + const entry = referenceAt(object, "entry", "$", issues); + const icon = referenceAt(object, "icon", "$", issues); + const categories = stringListAt(object, "categories", "$", issues); + const tags = stringListAt(object, "tags", "$", issues); + const integration = parseIntegration( + object.integration, + "$.integration", + issues, + ); + const requirements = parseRequirements( + object.requirements, + "$.requirements", + issues, + ); + const privacy = parsePrivacy(object.privacy, "$.privacy", issues); + const source = + "source" in object + ? parseSource(object.source, "$.source", issues) + : undefined; + const actions = + "actions" in object + ? parseActions(object.actions, "$.actions", issues) + : undefined; + const assets = + "assets" in object + ? parseAssets(object.assets, "$.assets", issues) + : undefined; + + if ( + issues.length > 0 || + id === undefined || + name === undefined || + version === undefined || + description === undefined || + entry === undefined || + icon === undefined || + integration === undefined || + requirements === undefined || + privacy === undefined + ) { + throw new ToolboxValidationError("invalid-app", issues); + } + return { + schemaVersion: TOOLBOX_SCHEMA_VERSION, + id, + name, + version, + description, + entry, + icon, + categories, + tags, + integration, + requirements, + privacy, + ...(source === undefined ? {} : { source }), + ...(actions === undefined ? {} : { actions }), + ...(assets === undefined ? {} : { assets }), + }; +} + +function parseCatalogEntry( + value: unknown, + path: string, + issues: string[], +): ToolboxCatalogEntry | undefined { + const object = recordAt(value, path, issues); + const enabled = booleanAt(object, "enabled", path, issues); + if ("manifest" in object) { + const manifest = referenceAt(object, "manifest", path, issues); + if ("entry" in object) { + issues.push(`${path} cannot contain both manifest and entry`); + } + return manifest === undefined || enabled === undefined + ? undefined + : ({ manifest, enabled } satisfies ToolboxCatalogManifestEntry); + } + const name = stringAt(object, "name", path, issues); + const entry = referenceAt(object, "entry", path, issues); + const launch = enumAt( + object, + "launch", + ["navigate", "new-tab"] as const, + path, + issues, + ); + return name === undefined || + entry === undefined || + launch === undefined || + enabled === undefined + ? undefined + : ({ name, entry, launch, enabled } satisfies ToolboxCatalogExternalEntry); +} + +function parseTheme( + value: unknown, + path: string, + issues: string[], +): ToolboxCatalogTheme | undefined { + const object = recordAt(value, path, issues); + const mode = enumAt( + object, + "mode", + ["light", "dark", "system"] as const, + path, + issues, + ); + const brand = stringAt(object, "brand", path, issues); + return mode === undefined || brand === undefined + ? undefined + : { mode, brand }; +} + +export function parseToolboxCatalog(value: unknown): ToolboxCatalog { + const issues: string[] = []; + const object = recordAt(value, "$", issues); + schemaVersionAt(object, "$", issues); + const id = idAt(object, "id", "$", issues); + const name = stringAt(object, "name", "$", issues); + const home = referenceAt(object, "home", "$", issues); + const theme = parseTheme(object.theme, "$.theme", issues); + const appsValue = object.apps; + let apps: readonly ToolboxCatalogEntry[] = []; + if (!Array.isArray(appsValue)) { + issues.push("$.apps must be an array"); + } else { + apps = appsValue.flatMap((entry, index) => { + const parsed = parseCatalogEntry(entry, `$.apps[${index}]`, issues); + return parsed === undefined ? [] : [parsed]; + }); + } + if ( + issues.length > 0 || + id === undefined || + name === undefined || + home === undefined || + theme === undefined + ) { + throw new ToolboxValidationError("invalid-catalog", issues); + } + return { + schemaVersion: TOOLBOX_SCHEMA_VERSION, + id, + name, + home, + theme, + apps, + }; +} + +export function defineToolboxApp( + app: T, +): T { + return app; +} + +export function defineToolboxCatalog( + catalog: T, +): T { + return catalog; +} diff --git a/packages/contract/src/types.ts b/packages/contract/src/types.ts new file mode 100644 index 0000000..699773e --- /dev/null +++ b/packages/contract/src/types.ts @@ -0,0 +1,178 @@ +export const TOOLBOX_SCHEMA_VERSION = 1 as const; +export const TOOLBOX_QUERY_PARAMETER = "toolbox" as const; +export const TOOLBOX_META_NAME = "toolbox" as const; + +export interface ToolboxAction { + id: string; + label: string; + url: string; +} + +export interface ToolboxPrivacy { + processing: "local" | "remote" | "mixed"; + fileUploads: boolean; + telemetry: boolean; + label?: string; + url?: string; +} + +export type ToolboxLaunchMode = "navigate" | "new-tab"; + +export interface ToolboxIntegration { + contextVersion: 1; + launchModes: readonly ToolboxLaunchMode[]; + embedding: "unsupported" | "supported"; +} + +export interface ToolboxRequirements { + secureContext: boolean; + workers: boolean; + indexedDb: boolean; + crossOriginIsolated: boolean; + topLevelContext?: boolean; +} + +export interface ToolboxSource { + repository: string; + license: string; +} + +export interface ToolboxAppManifest { + schemaVersion: typeof TOOLBOX_SCHEMA_VERSION; + id: string; + name: string; + version: string; + description: string; + entry: string; + icon: string; + categories: readonly string[]; + tags: readonly string[]; + integration: ToolboxIntegration; + requirements: ToolboxRequirements; + privacy: ToolboxPrivacy; + source?: ToolboxSource; + actions?: readonly ToolboxAction[]; + assets?: readonly string[]; +} + +export interface ToolboxCatalogManifestEntry { + manifest: string; + enabled: boolean; +} + +export interface ToolboxCatalogExternalEntry { + name: string; + entry: string; + launch: ToolboxLaunchMode; + enabled: boolean; +} + +export type ToolboxCatalogEntry = + ToolboxCatalogManifestEntry | ToolboxCatalogExternalEntry; + +export interface ToolboxCatalogTheme { + mode: "light" | "dark" | "system"; + brand: string; +} + +export interface ToolboxCatalog { + schemaVersion: typeof TOOLBOX_SCHEMA_VERSION; + id: string; + name: string; + home: string; + theme: ToolboxCatalogTheme; + apps: readonly ToolboxCatalogEntry[]; +} + +export interface ResolvedToolboxAction extends Omit { + url: URL; +} + +export interface ResolvedToolboxPrivacy extends Omit { + url?: URL; +} + +export interface ResolvedToolboxApp { + manifest: ToolboxAppManifest; + manifestUrl: URL; + entryUrl: URL; + actions: readonly ResolvedToolboxAction[]; + assetUrls: readonly URL[]; + iconUrl: URL; + privacy: ResolvedToolboxPrivacy; +} + +export interface ResolvedToolboxCatalogManifestEntry { + kind: "manifest"; + enabled: boolean; + app: ResolvedToolboxApp; +} + +export interface ResolvedToolboxCatalogExternalEntry { + kind: "external"; + enabled: boolean; + name: string; + entryUrl: URL; + launch: ToolboxLaunchMode; +} + +export type ResolvedToolboxCatalogEntry = + ResolvedToolboxCatalogManifestEntry | ResolvedToolboxCatalogExternalEntry; + +export interface ResolvedToolboxCatalog { + catalog: ToolboxCatalog; + catalogUrl: URL; + apps: readonly ResolvedToolboxCatalogEntry[]; + homeUrl: URL; +} + +export type ToolboxDiscoverySource = "query" | "meta" | "explicit"; + +export interface ToolboxContext { + source: ToolboxDiscoverySource; + catalog: ResolvedToolboxCatalog; +} + +export type ToolboxErrorCode = + | "invalid-app" + | "invalid-catalog" + | "invalid-url" + | "cross-origin" + | "fetch-failed"; + +export class ToolboxError extends Error { + readonly code: ToolboxErrorCode; + + constructor(code: ToolboxErrorCode, message: string, options?: ErrorOptions) { + super(message, options); + this.name = "ToolboxError"; + this.code = code; + } +} + +export class ToolboxValidationError extends ToolboxError { + readonly issues: readonly string[]; + + constructor( + code: Extract, + issues: readonly string[], + ) { + super(code, `Invalid toolbox document: ${issues.join("; ")}`); + this.name = "ToolboxValidationError"; + this.issues = issues; + } +} + +export type ToolboxDiscovery = + | { status: "none" } + | { + status: "found"; + source: Exclude; + catalogUrl: URL; + } + | { status: "error"; error: ToolboxError }; + +export type ToolboxContextResult = + | { status: "standalone" } + | { status: "ready"; context: ToolboxContext } + | { status: "error"; error: ToolboxError }; diff --git a/packages/contract/src/urls.ts b/packages/contract/src/urls.ts new file mode 100644 index 0000000..ae42d32 --- /dev/null +++ b/packages/contract/src/urls.ts @@ -0,0 +1,131 @@ +import { + TOOLBOX_QUERY_PARAMETER, + ToolboxError, + type ResolvedToolboxAction, + type ResolvedToolboxApp, + type ResolvedToolboxPrivacy, + type ToolboxAppManifest, +} from "./types.js"; + +const WEB_PROTOCOLS = new Set(["http:", "https:"]); + +export function resolveWebUrl( + reference: string | URL, + base?: string | URL, +): URL { + let url: URL; + try { + url = base === undefined ? new URL(reference) : new URL(reference, base); + } catch (cause) { + throw new ToolboxError("invalid-url", `Invalid URL: ${String(reference)}`, { + cause, + }); + } + if (!WEB_PROTOCOLS.has(url.protocol) || url.username || url.password) { + throw new ToolboxError( + "invalid-url", + `Only credential-free HTTP(S) URLs are supported: ${url.href}`, + ); + } + return url; +} + +export function requireSameOrigin( + url: URL, + expected: URL, + label = "Toolbox URL", +): URL { + if (url.origin !== expected.origin) { + throw new ToolboxError( + "cross-origin", + `${label} must share the page origin (${expected.origin})`, + ); + } + return url; +} + +export function resolveToolboxApp( + manifest: ToolboxAppManifest, + manifestUrl: string | URL, + expectedOrigin?: string | URL, +): ResolvedToolboxApp { + const resolvedManifestUrl = resolveWebUrl(manifestUrl); + const origin = + expectedOrigin === undefined + ? resolvedManifestUrl + : resolveWebUrl(expectedOrigin); + + requireSameOrigin(resolvedManifestUrl, origin, "Application manifest"); + + const localUrl = (reference: string, label: string): URL => + requireSameOrigin( + resolveWebUrl(reference, resolvedManifestUrl), + origin, + label, + ); + const externalUrl = (reference: string): URL => + resolveWebUrl(reference, resolvedManifestUrl); + + const actions: readonly ResolvedToolboxAction[] = ( + manifest.actions ?? [] + ).map((action) => ({ + ...action, + url: externalUrl(action.url), + })); + const privacy: ResolvedToolboxPrivacy = { + processing: manifest.privacy.processing, + fileUploads: manifest.privacy.fileUploads, + telemetry: manifest.privacy.telemetry, + ...(manifest.privacy.label === undefined + ? {} + : { label: manifest.privacy.label }), + ...(manifest.privacy.url === undefined + ? {} + : { url: externalUrl(manifest.privacy.url) }), + }; + + return { + manifest, + manifestUrl: resolvedManifestUrl, + entryUrl: localUrl(manifest.entry, "Application entry"), + actions, + assetUrls: (manifest.assets ?? []).map((asset) => + localUrl(asset, "Application asset"), + ), + iconUrl: localUrl(manifest.icon, "Application icon"), + privacy, + }; +} + +export interface ContextualLinkOptions { + location?: string | URL; + parameter?: string; +} + +export function contextualizeToolboxLink( + target: string | URL, + catalog: string | URL, + options: ContextualLinkOptions = {}, +): string { + const location = resolveWebUrl( + options.location ?? + (typeof globalThis.location === "undefined" + ? "http://localhost/" + : globalThis.location.href), + ); + const targetUrl = resolveWebUrl(target, location); + if (targetUrl.origin !== location.origin) return targetUrl.href; + + const catalogUrl = requireSameOrigin( + resolveWebUrl(catalog, location), + location, + "Toolbox catalog", + ); + targetUrl.searchParams.set( + options.parameter ?? TOOLBOX_QUERY_PARAMETER, + catalogUrl.href, + ); + return targetUrl.href; +} + +export const createContextualLink = contextualizeToolboxLink; diff --git a/packages/contract/test/contract.test.ts b/packages/contract/test/contract.test.ts new file mode 100644 index 0000000..23581b3 --- /dev/null +++ b/packages/contract/test/contract.test.ts @@ -0,0 +1,288 @@ +import { + ToolboxValidationError, + contextualizeToolboxLink, + discoverToolboxCatalog, + loadToolboxContext, + parseToolboxApp, + parseToolboxCatalog, + type ToolboxAppManifest, +} from "../src/index.js"; +import { describe, expect, it, vi } from "vitest"; + +const app = ( + overrides: Record = {}, +): Record => ({ + schemaVersion: 1, + id: "de.add-ideas.pdf-tools", + name: "PDF Workbench", + version: "1.2.3", + description: "Local PDF tools", + entry: "./index.html", + icon: "./icon.svg", + categories: ["documents", "pdf"], + tags: ["merge", "split"], + 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.example.test/pdf-tools", + license: "AGPL-3.0-only", + }, + ...overrides, +}); + +const catalog = ( + overrides: Record = {}, +): Record => ({ + schemaVersion: 1, + id: "de.add-ideas.toolbox", + name: "Toolbox", + home: "./", + theme: { mode: "system", brand: "add·ideas" }, + apps: [{ manifest: "./pdf/toolbox-app.json", enabled: true }], + ...overrides, +}); + +const json = (value: unknown, init: ResponseInit = {}): Response => + new Response(JSON.stringify(value), { + status: 200, + headers: { "content-type": "application/json" }, + ...init, + }); + +describe("v1 runtime parsing", () => { + it("parses the full app contract and drops unknown optional fields", () => { + const parsed = parseToolboxApp( + app({ + futureField: "ignored", + privacy: { + processing: "local", + fileUploads: false, + telemetry: false, + futurePrivacyField: true, + }, + }), + ); + + expect(parsed).toMatchObject({ + schemaVersion: 1, + id: "de.add-ideas.pdf-tools", + privacy: { processing: "local", fileUploads: false, telemetry: false }, + }); + expect(parsed).not.toHaveProperty("futureField"); + expect(parsed.privacy).not.toHaveProperty("futurePrivacyField"); + }); + + it("rejects unsupported versions and malformed known optional fields", () => { + expect(() => parseToolboxApp(app({ schemaVersion: 2 }))).toThrow( + ToolboxValidationError, + ); + expect(() => + parseToolboxApp( + app({ + requirements: { + secureContext: true, + workers: true, + indexedDb: true, + crossOriginIsolated: false, + topLevelContext: "sometimes", + }, + }), + ), + ).toThrow(/topLevelContext must be a boolean/u); + }); + + it("accepts omitted source metadata but validates it when supplied", () => { + const withoutSource = app(); + delete withoutSource.source; + expect(parseToolboxApp(withoutSource)).not.toHaveProperty("source"); + expect(() => + parseToolboxApp( + app({ source: { repository: "../repository", license: "MIT" } }), + ), + ).toThrow(/absolute HTTP\(S\) URL/u); + }); + + it("parses manifest references and external inline catalog entries", () => { + const parsed = parseToolboxCatalog( + catalog({ + futureField: 1, + apps: [ + { manifest: "./pdf/toolbox-app.json", enabled: true, future: true }, + { + name: "External docs", + entry: "https://docs.example.test/", + launch: "new-tab", + enabled: true, + }, + ], + }), + ); + + expect(parsed.apps).toHaveLength(2); + expect(parsed.apps[1]).toEqual({ + name: "External docs", + entry: "https://docs.example.test/", + launch: "new-tab", + enabled: true, + }); + expect(parsed).not.toHaveProperty("futureField"); + }); +}); + +describe("same-origin discovery", () => { + it("prefers a same-origin query context over meta context", () => { + const result = discoverToolboxCatalog({ + location: "https://tools.example.test/pdf/?toolbox=%2Fcatalog.json", + document: { + querySelector: () => ({ + getAttribute: () => "/meta-catalog.json", + }), + }, + }); + + expect(result.status).toBe("found"); + if (result.status === "found") { + expect(result.source).toBe("query"); + expect(result.catalogUrl.href).toBe( + "https://tools.example.test/catalog.json", + ); + } + }); + + it("rejects a cross-origin query without falling through to meta", () => { + const result = discoverToolboxCatalog({ + location: + "https://tools.example.test/pdf/?toolbox=https%3A%2F%2Fevil.test%2Fcatalog.json", + document: { + querySelector: () => ({ getAttribute: () => "/safe.json" }), + }, + }); + + expect(result.status).toBe("error"); + if (result.status === "error") + expect(result.error.code).toBe("cross-origin"); + }); +}); + +describe("loading and links", () => { + it("loads a catalog, resolves manifests and URLs, and supports external apps", async () => { + const fetch = vi.fn( + async (input: string | URL | Request, init?: RequestInit) => { + void init; + const url = new URL(input instanceof Request ? input.url : input); + if (url.pathname === "/catalog.json") { + return json( + catalog({ + apps: [ + { manifest: "./pdf/toolbox-app.json", enabled: true }, + { manifest: "./disabled/toolbox-app.json", enabled: false }, + { + name: "Documentation", + entry: "https://docs.example.test/", + launch: "new-tab", + enabled: true, + }, + ], + }), + ); + } + if (url.pathname === "/pdf/toolbox-app.json") return json(app()); + if (url.pathname === "/disabled/toolbox-app.json") { + throw new Error("Disabled manifests must not be fetched"); + } + return new Response("Not found", { status: 404 }); + }, + ); + + const result = await loadToolboxContext({ + location: "https://tools.example.test/pdf/?toolbox=/catalog.json", + fetch, + }); + + for (const [, init] of fetch.mock.calls) { + expect(init).toMatchObject({ redirect: "error" }); + } + + expect(result.status).toBe("ready"); + if (result.status === "ready") { + expect(result.context.catalog.homeUrl.href).toBe( + "https://tools.example.test/", + ); + expect(result.context.catalog.apps[0]?.kind).toBe("manifest"); + const first = result.context.catalog.apps[0]; + if (first?.kind === "manifest") { + expect(first.app.entryUrl.href).toBe( + "https://tools.example.test/pdf/index.html", + ); + } + expect(result.context.catalog.apps[1]).toMatchObject({ + kind: "external", + launch: "new-tab", + }); + } + }); + + it("returns a graceful error result for invalid JSON and fetch failure", async () => { + const invalid = await loadToolboxContext({ + location: "https://tools.example.test/?toolbox=/catalog.json", + fetch: async () => json(catalog({ schemaVersion: 2 })), + }); + expect(invalid.status).toBe("error"); + + const unavailable = await loadToolboxContext({ + location: "https://tools.example.test/?toolbox=/catalog.json", + fetch: async () => new Response("No", { status: 503 }), + }); + expect(unavailable.status).toBe("error"); + if (unavailable.status === "error") { + expect(unavailable.error.code).toBe("fetch-failed"); + } + }); + + it("rejects a fetch redirected to a cross-origin response URL", async () => { + const response = json(catalog()); + Object.defineProperty(response, "url", { + value: "https://evil.example/toolbox.catalog.json", + }); + const result = await loadToolboxContext({ + location: "https://tools.example.test/?toolbox=/catalog.json", + fetch: async () => response, + }); + + expect(result.status).toBe("error"); + if (result.status === "error") + expect(result.error.code).toBe("cross-origin"); + }); + + it("adds context to same-origin links but leaves external links alone", () => { + expect( + new URL( + contextualizeToolboxLink("./other/", "/catalog.json", { + location: "https://tools.example.test/pdf/", + }), + ).searchParams.get("toolbox"), + ).toBe("https://tools.example.test/catalog.json"); + + expect( + contextualizeToolboxLink("https://docs.example.test/", "/catalog.json", { + location: "https://tools.example.test/pdf/", + }), + ).toBe("https://docs.example.test/"); + }); +}); + +export const validAppFixture = app() as unknown as ToolboxAppManifest; diff --git a/packages/contract/test/schema-parity.test.ts b/packages/contract/test/schema-parity.test.ts new file mode 100644 index 0000000..b5fb6a1 --- /dev/null +++ b/packages/contract/test/schema-parity.test.ts @@ -0,0 +1,261 @@ +// @vitest-environment node + +import { readFileSync } from "node:fs"; + +import Ajv2020, { type ValidateFunction } from "ajv/dist/2020.js"; +import { describe, expect, it } from "vitest"; + +import { parseToolboxApp, parseToolboxCatalog } from "../src/index.js"; + +const appSchema = JSON.parse( + readFileSync( + new URL("../../../schemas/toolbox-app.v1.schema.json", import.meta.url), + "utf8", + ), +) as object; +const catalogSchema = JSON.parse( + readFileSync( + new URL("../../../schemas/toolbox-catalog.v1.schema.json", import.meta.url), + "utf8", + ), +) as object; + +const ajv = new Ajv2020({ + allErrors: true, + strict: true, + // `not: { required: [...] }` intentionally names the opposite union arm. + strictRequired: false, +}); +const validateApp = ajv.compile(appSchema); +const validateCatalog = ajv.compile(catalogSchema); + +type Parser = (value: unknown) => unknown; + +function runtimeAccepts(parser: Parser, value: unknown): boolean { + try { + parser(value); + return true; + } catch { + return false; + } +} + +function expectParity( + validate: ValidateFunction, + parser: Parser, + value: unknown, + expected: boolean, +): void { + const schemaAccepted = validate(value); + expect( + schemaAccepted, + ajv.errorsText(validate.errors, { separator: "\n" }), + ).toBe(expected); + expect(runtimeAccepts(parser, value)).toBe(expected); +} + +const validApp = (): Record => ({ + schemaVersion: 1, + id: "de.add-ideas.example", + name: "Example", + version: "1.2.3", + description: "Example application", + entry: "./", + icon: "./icon.svg", + categories: ["documents"], + tags: ["example"], + integration: { + contextVersion: 1, + launchModes: ["navigate"], + embedding: "unsupported", + }, + requirements: { + secureContext: false, + workers: false, + indexedDb: false, + crossOriginIsolated: false, + }, + privacy: { + processing: "local", + fileUploads: false, + telemetry: false, + }, + source: { + repository: "https://example.test/source", + license: "MIT", + }, +}); + +const validCatalog = (): Record => ({ + schemaVersion: 1, + id: "de.add-ideas.toolbox", + name: "Example Toolbox", + home: "./", + theme: { mode: "system", brand: "Example" }, + apps: [{ manifest: "./apps/example/toolbox-app.json", enabled: true }], +}); + +describe("canonical schema and runtime parser parity", () => { + const acceptedApps: Array<[string, () => Record]> = [ + ["the baseline app", validApp], + [ + "relative references containing spaces and path @ signs", + () => ({ + ...validApp(), + entry: " ./nested app/?mode=1#result ", + actions: [{ id: "docs", label: "Docs", url: "./people/@me" }], + }), + ], + [ + "case-insensitive and compact absolute HTTP URLs", + () => ({ + ...validApp(), + source: { repository: "HTTPS:example.test", license: "MIT" }, + }), + ], + ]; + + it.each(acceptedApps)("accepts %s", (_label, fixture) => { + expectParity(validateApp, parseToolboxApp, fixture(), true); + }); + + const rejectedApps: Array<[string, () => Record]> = [ + ["a whitespace-only name", () => ({ ...validApp(), name: " \t " })], + [ + "a whitespace-only list item", + () => ({ ...validApp(), categories: ["documents", " \n "] }), + ], + [ + "a non-web entry scheme", + () => ({ ...validApp(), entry: "javascript:alert(1)" }), + ], + ["a backslash path", () => ({ ...validApp(), icon: ".\\icon.svg" })], + ["an empty HTTP authority", () => ({ ...validApp(), entry: "https://" })], + [ + "a credential-bearing privacy URL", + () => ({ + ...validApp(), + privacy: { + processing: "local", + fileUploads: false, + telemetry: false, + url: "https://user:secret@example.test/privacy", + }, + }), + ], + [ + "a relative source repository", + () => ({ + ...validApp(), + source: { repository: "../source", license: "MIT" }, + }), + ], + [ + "a compact credential-bearing source repository", + () => ({ + ...validApp(), + source: { repository: "https:user@example.test", license: "MIT" }, + }), + ], + [ + "a whitespace-only action label", + () => ({ + ...validApp(), + actions: [{ id: "docs", label: " ", url: "./docs" }], + }), + ], + ]; + + it.each(rejectedApps)("rejects %s", (_label, fixture) => { + expectParity(validateApp, parseToolboxApp, fixture(), false); + }); + + const acceptedCatalogs: Array<[string, () => Record]> = [ + ["the baseline catalog", validCatalog], + [ + "an uppercase HTTPS manifest URL", + () => ({ + ...validCatalog(), + apps: [ + { + manifest: "HTTPS://example.test/apps/example/toolbox-app.json", + enabled: true, + }, + ], + }), + ], + ]; + + it.each(acceptedCatalogs)("accepts %s", (_label, fixture) => { + expectParity(validateCatalog, parseToolboxCatalog, fixture(), true); + }); + + const rejectedCatalogs: Array<[string, () => Record]> = [ + [ + "a whitespace-only catalog name", + () => ({ ...validCatalog(), name: " " }), + ], + [ + "a whitespace-only brand", + () => ({ + ...validCatalog(), + theme: { mode: "system", brand: "\t" }, + }), + ], + [ + "a non-web home URL", + () => ({ ...validCatalog(), home: "file:///tmp/toolbox" }), + ], + [ + "a backslash manifest path", + () => ({ + ...validCatalog(), + apps: [{ manifest: ".\\apps\\toolbox-app.json", enabled: true }], + }), + ], + [ + "a credential-bearing manifest URL", + () => ({ + ...validCatalog(), + apps: [ + { + manifest: "https://user:secret@example.test/toolbox-app.json", + enabled: true, + }, + ], + }), + ], + [ + "a non-web external entry", + () => ({ + ...validCatalog(), + apps: [ + { + name: "External", + entry: "data:text/html,hello", + launch: "new-tab", + enabled: true, + }, + ], + }), + ], + [ + "a whitespace-only external name", + () => ({ + ...validCatalog(), + apps: [ + { + name: " ", + entry: "https://example.test/", + launch: "new-tab", + enabled: true, + }, + ], + }), + ], + ]; + + it.each(rejectedCatalogs)("rejects %s", (_label, fixture) => { + expectParity(validateCatalog, parseToolboxCatalog, fixture(), false); + }); +}); diff --git a/packages/contract/tsconfig.build.json b/packages/contract/tsconfig.build.json new file mode 100644 index 0000000..1db9c8a --- /dev/null +++ b/packages/contract/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false + }, + "exclude": ["test"] +} diff --git a/packages/contract/tsconfig.json b/packages/contract/tsconfig.json new file mode 100644 index 0000000..5285d28 --- /dev/null +++ b/packages/contract/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/shell-react/LICENSE b/packages/shell-react/LICENSE new file mode 100644 index 0000000..c38585f --- /dev/null +++ b/packages/shell-react/LICENSE @@ -0,0 +1,192 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + Copyright 2026 ADD Ideas + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/shell-react/README.md b/packages/shell-react/README.md new file mode 100644 index 0000000..3927689 --- /dev/null +++ b/packages/shell-react/README.md @@ -0,0 +1,15 @@ +# @add-ideas/toolbox-shell-react + +```tsx +import { AppShell } from "@add-ideas/toolbox-shell-react"; +import "@add-ideas/toolbox-shell-react/styles.css"; + +Help}> + +; +``` + +`AppShell` discovers `?toolbox=...` or ``, but continues in +standalone mode if context is absent, unavailable, cross-origin, or invalid. Its +app switcher is exposed as the accessible navigation landmark +`Toolbox applications` and all destinations use ordinary document navigation. diff --git a/packages/shell-react/package.json b/packages/shell-react/package.json new file mode 100644 index 0000000..f766439 --- /dev/null +++ b/packages/shell-react/package.json @@ -0,0 +1,53 @@ +{ + "name": "@add-ideas/toolbox-shell-react", + "version": "0.1.0", + "description": "A lightweight React application shell for toolbox-compatible browser applications.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://git.add-ideas.de/zemion/toolbox-sdk.git", + "directory": "packages/shell-react" + }, + "homepage": "https://git.add-ideas.de/zemion/toolbox-sdk", + "keywords": [ + "toolbox", + "react", + "application-shell", + "typescript" + ], + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./styles.css": "./dist/styles.css", + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "peerDependencies": { + "react": ">=18 <20", + "react-dom": ">=18 <20" + }, + "dependencies": { + "@add-ideas/toolbox-contract": "0.1.0" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');fs.copyFileSync('src/styles.css','dist/styles.css');fs.copyFileSync('../../LICENSE','LICENSE')\"", + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "prepack": "npm run build", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "publishConfig": { + "access": "public", + "registry": "https://git.add-ideas.de/api/packages/zemion/npm/" + } +} diff --git a/packages/shell-react/src/AppShell.tsx b/packages/shell-react/src/AppShell.tsx new file mode 100644 index 0000000..1294d7d --- /dev/null +++ b/packages/shell-react/src/AppShell.tsx @@ -0,0 +1,231 @@ +import { + contextualizeToolboxLink, + loadToolboxContext, + resolveWebUrl, + type ToolboxAppManifest, + type ToolboxContext, + type ToolboxContextLoadOptions, + type ToolboxError, +} from "@add-ideas/toolbox-contract"; +import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; + +export interface AppShellProps { + app: ToolboxAppManifest; + children: ReactNode; + appActions?: ReactNode; + className?: string; + manifestUrl?: string | URL; + contextOptions?: Omit; + onContextError?: (error: ToolboxError) => void; +} + +function currentHref(): string { + return typeof globalThis.location === "undefined" + ? "http://localhost/" + : globalThis.location.href; +} + +function safeHref(reference: string, base: string | URL): string | undefined { + try { + return resolveWebUrl(reference, base).href; + } catch { + return undefined; + } +} + +function privacyText(app: ToolboxAppManifest): string { + if (app.privacy.label) return app.privacy.label; + const processing = + app.privacy.processing === "local" + ? "Local processing" + : app.privacy.processing === "remote" + ? "Remote processing" + : "Local and remote processing"; + const details = [ + app.privacy.fileUploads ? "uploads files" : "no file uploads", + app.privacy.telemetry ? "uses telemetry" : "no telemetry", + ]; + return `${processing} · ${details.join(" · ")}`; +} + +export function AppShell({ + app, + children, + appActions, + className, + manifestUrl, + contextOptions, + onContextError, +}: AppShellProps) { + const [context, setContext] = useState(null); + const [contextState, setContextState] = useState<"standalone" | "connected">( + "standalone", + ); + const errorHandler = useRef(onContextError); + + useEffect(() => { + errorHandler.current = onContextError; + }, [onContextError]); + + const contextCatalog = contextOptions?.catalogUrl; + const contextDocument = contextOptions?.document; + const contextFetch = contextOptions?.fetch; + const contextLocation = contextOptions?.location; + const contextMetaName = contextOptions?.metaName; + const contextQueryParameter = contextOptions?.queryParameter; + + useEffect(() => { + const controller = new AbortController(); + void loadToolboxContext({ + ...(contextCatalog === undefined ? {} : { catalogUrl: contextCatalog }), + ...(contextDocument === undefined ? {} : { document: contextDocument }), + ...(contextFetch === undefined ? {} : { fetch: contextFetch }), + ...(contextLocation === undefined ? {} : { location: contextLocation }), + ...(contextMetaName === undefined ? {} : { metaName: contextMetaName }), + ...(contextQueryParameter === undefined + ? {} + : { queryParameter: contextQueryParameter }), + signal: controller.signal, + }).then((result) => { + if (controller.signal.aborted) return; + if (result.status === "ready") { + setContext(result.context); + setContextState("connected"); + } else { + setContext(null); + setContextState("standalone"); + if (result.status === "error") errorHandler.current?.(result.error); + } + }); + return () => { + controller.abort(); + }; + }, [ + contextCatalog, + contextDocument, + contextFetch, + contextLocation, + contextMetaName, + contextQueryParameter, + ]); + + const pageUrl = contextLocation ?? currentHref(); + const appManifestUrl = useMemo( + () => + manifestUrl ?? + safeHref("toolbox-app.json", pageUrl) ?? + "http://localhost/toolbox-app.json", + [manifestUrl, pageUrl], + ); + const privacyHref = app.privacy.url + ? safeHref(app.privacy.url, appManifestUrl) + : undefined; + const enabledApps = + context?.catalog.apps.filter((entry) => entry.enabled) ?? []; + const theme = context?.catalog.catalog.theme.mode ?? "system"; + const rootClassName = ["toolbox-shell", className].filter(Boolean).join(" "); + + return ( +
+
+
+ {context ? ( + + {context.catalog.catalog.theme.brand} + + ) : null} + +
+ +
+

{app.name}

+ + {app.description} + +
+
+ +
+ {app.actions?.map((action) => { + const href = safeHref(action.url, appManifestUrl); + return href ? ( + + {action.label} + + ) : null; + })} + {appActions} +
+ +
+ v{app.version} + {privacyHref ? ( + {privacyText(app)} + ) : ( + {privacyText(app)} + )} +
+ + {context && enabledApps.length > 0 ? ( +
+ Apps + +
+ ) : null} +
+
+
{children}
+
+ ); +} diff --git a/packages/shell-react/src/index.ts b/packages/shell-react/src/index.ts new file mode 100644 index 0000000..2cdf94c --- /dev/null +++ b/packages/shell-react/src/index.ts @@ -0,0 +1,2 @@ +export { AppShell } from "./AppShell.js"; +export type { AppShellProps } from "./AppShell.js"; diff --git a/packages/shell-react/src/styles.css b/packages/shell-react/src/styles.css new file mode 100644 index 0000000..6b308e9 --- /dev/null +++ b/packages/shell-react/src/styles.css @@ -0,0 +1,202 @@ +:root { + --toolbox-font: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", sans-serif; + --toolbox-background: #f5f7fa; + --toolbox-surface: #ffffff; + --toolbox-text: #172033; + --toolbox-muted: #657086; + --toolbox-border: #dce2ea; + --toolbox-accent: #3156d3; + --toolbox-accent-contrast: #ffffff; + --toolbox-radius: 0.65rem; + --toolbox-shadow: 0 1px 2px rgb(18 28 45 / 8%); +} + +[data-toolbox-theme="dark"] { + --toolbox-background: #111722; + --toolbox-surface: #192130; + --toolbox-text: #edf1f7; + --toolbox-muted: #aab4c5; + --toolbox-border: #303b4d; + --toolbox-accent: #9db2ff; + --toolbox-accent-contrast: #111722; +} + +@media (prefers-color-scheme: dark) { + [data-toolbox-theme="system"] { + --toolbox-background: #111722; + --toolbox-surface: #192130; + --toolbox-text: #edf1f7; + --toolbox-muted: #aab4c5; + --toolbox-border: #303b4d; + --toolbox-accent: #9db2ff; + --toolbox-accent-contrast: #111722; + } +} + +.toolbox-shell { + min-height: 100vh; + color: var(--toolbox-text); + background: var(--toolbox-background); + font-family: var(--toolbox-font); +} + +.toolbox-shell *, +.toolbox-shell *::before, +.toolbox-shell *::after { + box-sizing: border-box; +} + +.toolbox-shell__header { + position: relative; + z-index: 10; + border-bottom: 1px solid var(--toolbox-border); + background: var(--toolbox-surface); + box-shadow: var(--toolbox-shadow); +} + +.toolbox-shell__bar { + display: flex; + min-height: 4.25rem; + align-items: center; + gap: 1rem; + max-width: 90rem; + margin: 0 auto; + padding: 0.75rem 1rem; +} + +.toolbox-shell a { + color: var(--toolbox-accent); + text-decoration: none; +} + +.toolbox-shell a:hover { + text-decoration: underline; +} + +.toolbox-shell__home { + flex: none; + font-weight: 700; +} + +.toolbox-shell__identity { + display: flex; + min-width: 12rem; + align-items: center; + gap: 0.65rem; +} + +.toolbox-shell__identity > div { + display: grid; + gap: 0.1rem; +} + +.toolbox-shell__icon { + flex: none; + border-radius: 0.35rem; +} + +.toolbox-shell__name { + margin: 0; + font-size: 1rem; + line-height: 1.2; +} + +.toolbox-shell__description { + color: var(--toolbox-muted); + font-size: 0.8rem; + line-height: 1.25; +} + +.toolbox-shell__actions { + display: flex; + align-items: center; + gap: 0.75rem; + margin-left: auto; +} + +.toolbox-shell__meta { + display: grid; + flex: none; + gap: 0.15rem; + color: var(--toolbox-muted); + font-size: 0.75rem; + text-align: right; +} + +.toolbox-shell__switcher { + position: relative; + flex: none; +} + +.toolbox-shell__switcher summary { + padding: 0.45rem 0.7rem; + border: 1px solid var(--toolbox-border); + border-radius: var(--toolbox-radius); + cursor: pointer; + font-weight: 600; + list-style: none; +} + +.toolbox-shell__switcher summary::-webkit-details-marker { + display: none; +} + +.toolbox-shell__switcher nav { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + width: max-content; + min-width: 12rem; + padding: 0.4rem; + border: 1px solid var(--toolbox-border); + border-radius: var(--toolbox-radius); + background: var(--toolbox-surface); + box-shadow: 0 0.75rem 2rem rgb(18 28 45 / 14%); +} + +.toolbox-shell__switcher ul { + display: grid; + gap: 0.15rem; + margin: 0; + padding: 0; + list-style: none; +} + +.toolbox-shell__switcher a { + display: block; + padding: 0.45rem 0.6rem; + border-radius: 0.4rem; +} + +.toolbox-shell__switcher a[aria-current="page"] { + color: var(--toolbox-accent-contrast); + background: var(--toolbox-accent); +} + +.toolbox-shell__main { + width: min(100%, 90rem); + margin: 0 auto; + padding: 1rem; +} + +@media (max-width: 52rem) { + .toolbox-shell__bar { + flex-wrap: wrap; + } + + .toolbox-shell__identity { + flex: 1; + } + + .toolbox-shell__actions { + order: 3; + width: 100%; + margin-left: 0; + } + + .toolbox-shell__meta { + display: none; + } +} diff --git a/packages/shell-react/test/AppShell.test.tsx b/packages/shell-react/test/AppShell.test.tsx new file mode 100644 index 0000000..a31ae41 --- /dev/null +++ b/packages/shell-react/test/AppShell.test.tsx @@ -0,0 +1,202 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import type { ToolboxAppManifest } from "@add-ideas/toolbox-contract"; +import { describe, expect, it, vi } from "vitest"; + +import { AppShell } from "../src/index.js"; + +const currentApp: ToolboxAppManifest = { + schemaVersion: 1, + id: "de.add-ideas.pdf-tools", + name: "PDF Workbench", + version: "1.2.3", + description: "Local PDF tools", + entry: "./index.html", + icon: "./icon.svg", + categories: ["documents", "pdf"], + tags: ["merge"], + 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.example.test/pdf-tools", + license: "AGPL-3.0-only", + }, +}; + +const otherApp: ToolboxAppManifest = { + ...currentApp, + id: "de.add-ideas.xslt-tools", + name: "XSLT Workbench", + entry: "./index.html", + source: { + repository: "https://git.example.test/xslt-tools", + license: "AGPL-3.0-only", + }, +}; + +const catalog = { + schemaVersion: 1, + id: "de.add-ideas.toolbox", + name: "Toolbox", + home: "./", + theme: { mode: "system", brand: "add·ideas" }, + apps: [ + { manifest: "./pdf/toolbox-app.json", enabled: true }, + { manifest: "./xslt/toolbox-app.json", enabled: true }, + ], +}; + +const json = (value: unknown, status = 200): Response => + new Response(JSON.stringify(value), { + status, + headers: { "content-type": "application/json" }, + }); + +function catalogFetch() { + return vi.fn(async (input: string | URL | Request) => { + const url = new URL(input instanceof Request ? input.url : input); + if (url.pathname === "/toolbox.catalog.json") return json(catalog); + if (url.pathname === "/pdf/toolbox-app.json") return json(currentApp); + if (url.pathname === "/xslt/toolbox-app.json") return json(otherApp); + return json({}, 404); + }); +} + +describe("AppShell", () => { + it("renders identity, privacy, version, actions, and content standalone", async () => { + const fetch = vi.fn(); + render( + Help} + contextOptions={{ + location: "https://tools.example.test/pdf/", + fetch, + }} + > +

Application content

+
, + ); + + expect( + screen.getByRole("heading", { level: 1, name: "PDF Workbench" }), + ).toBeInTheDocument(); + expect(screen.getByLabelText("Version 1.2.3")).toBeInTheDocument(); + expect(screen.getByText(/Local processing/u)).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Help" })).toHaveAttribute( + "href", + "/help", + ); + expect(screen.getByText("Application content")).toBeInTheDocument(); + await waitFor(() => expect(fetch).not.toHaveBeenCalled()); + expect( + document.querySelector("[data-toolbox-context='standalone']"), + ).toBeInTheDocument(); + expect( + screen.queryByRole("navigation", { name: "Toolbox applications" }), + ).not.toBeInTheDocument(); + }); + + it("loads same-origin context and creates full-page contextual switch links", async () => { + const fetch = catalogFetch(); + render( + + PDF + , + ); + + const navigation = await screen.findByRole("navigation", { + name: "Toolbox applications", + }); + expect(navigation).toBeInTheDocument(); + const current = screen.getByRole("link", { name: "PDF Workbench" }); + expect(current).toHaveAttribute("aria-current", "page"); + const other = screen.getByRole("link", { name: "XSLT Workbench" }); + const destination = new URL(other.getAttribute("href") as string); + expect(destination.pathname).toBe("/xslt/index.html"); + expect(destination.searchParams.get("toolbox")).toBe( + "https://tools.example.test/toolbox.catalog.json", + ); + expect(screen.getByRole("link", { name: "add·ideas" })).toHaveAttribute( + "href", + "https://tools.example.test/", + ); + expect( + document.querySelector("[data-toolbox-context='connected']"), + ).toBeInTheDocument(); + }); + + it("rejects cross-origin context and remains standalone", async () => { + const fetch = vi.fn(); + const onContextError = vi.fn(); + render( + + PDF + , + ); + + await waitFor(() => expect(onContextError).toHaveBeenCalledOnce()); + expect(onContextError.mock.calls[0]?.[0]).toMatchObject({ + code: "cross-origin", + }); + expect(fetch).not.toHaveBeenCalled(); + expect( + screen.queryByRole("navigation", { name: "Toolbox applications" }), + ).not.toBeInTheDocument(); + }); + + it.each([ + ["invalid catalog", async () => json({ schemaVersion: 2 })], + ["fetch failure", async () => json({}, 503)], + ])("survives %s in standalone mode", async (_label, fetchImplementation) => { + const onContextError = vi.fn(); + render( + + Still usable + , + ); + + await waitFor(() => expect(onContextError).toHaveBeenCalledOnce()); + expect(screen.getByText("Still usable")).toBeInTheDocument(); + expect( + document.querySelector("[data-toolbox-context='standalone']"), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/shell-react/tsconfig.build.json b/packages/shell-react/tsconfig.build.json new file mode 100644 index 0000000..66aa131 --- /dev/null +++ b/packages/shell-react/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/shell-react/tsconfig.json b/packages/shell-react/tsconfig.json new file mode 100644 index 0000000..88b21c4 --- /dev/null +++ b/packages/shell-react/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "noEmit": true, + "paths": { + "@add-ideas/toolbox-contract": ["../contract/src/index.ts"] + } + }, + "include": ["src"] +} diff --git a/packages/testkit/LICENSE b/packages/testkit/LICENSE new file mode 100644 index 0000000..c38585f --- /dev/null +++ b/packages/testkit/LICENSE @@ -0,0 +1,192 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + Copyright 2026 ADD Ideas + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/testkit/README.md b/packages/testkit/README.md new file mode 100644 index 0000000..c75e61b --- /dev/null +++ b/packages/testkit/README.md @@ -0,0 +1,17 @@ +# @add-ideas/toolbox-testkit + +Validate and smoke-check a built toolbox application: + +```sh +toolbox-check dist +``` + +The checker validates `toolbox-app.json`, its id and SemVer version, verifies +the entry, icon, and declared assets, rejects unsafe local paths, serves the +build under a deep nested prefix, and fetches it both standalone and with +`?toolbox=/toolbox.catalog.json`. It also fetches local scripts, stylesheets, +images, icons, linked web manifests, and their icons from the entry HTML, so +root-absolute references fail the nested-deployment check. It does not launch a +browser. + +The same behavior is available programmatically as `checkToolboxDist()`. diff --git a/packages/testkit/package.json b/packages/testkit/package.json new file mode 100644 index 0000000..866f50c --- /dev/null +++ b/packages/testkit/package.json @@ -0,0 +1,52 @@ +{ + "name": "@add-ideas/toolbox-testkit", + "version": "0.1.0", + "description": "Manifest, asset, and nested-deployment smoke checks for built toolbox applications.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://git.add-ideas.de/zemion/toolbox-sdk.git", + "directory": "packages/testkit" + }, + "homepage": "https://git.add-ideas.de/zemion/toolbox-sdk", + "keywords": [ + "toolbox", + "manifest", + "smoke-test", + "cli" + ], + "type": "module", + "sideEffects": false, + "engines": { + "node": ">=20" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "bin": { + "toolbox-check": "./dist/cli.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "dependencies": { + "@add-ideas/toolbox-contract": "0.1.0" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');fs.chmodSync('dist/cli.js',0o755);fs.copyFileSync('../../LICENSE','LICENSE')\"", + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "prepack": "npm run build", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "publishConfig": { + "access": "public", + "registry": "https://git.add-ideas.de/api/packages/zemion/npm/" + } +} diff --git a/packages/testkit/src/check.ts b/packages/testkit/src/check.ts new file mode 100644 index 0000000..ce32ea1 --- /dev/null +++ b/packages/testkit/src/check.ts @@ -0,0 +1,482 @@ +import { + loadToolboxContext, + parseToolboxApp, + type ToolboxAppManifest, +} from "@add-ideas/toolbox-contract"; +import { createReadStream } from "node:fs"; +import { lstat, readFile, realpath, stat } from "node:fs/promises"; +import { createServer, type Server } from "node:http"; +import { extname, relative, resolve, sep } from "node:path"; + +const DEEP_PREFIX = "/__toolbox-check__/deep/nested/app/"; +const CATALOG_PATH = "/toolbox.catalog.json"; +const SEMVER_PATTERN = + /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + +export class ToolboxCheckError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "ToolboxCheckError"; + } +} + +export interface ToolboxCheckOptions { + manifestName?: string; +} + +export interface ToolboxCheckReport { + root: string; + manifestPath: string; + app: ToolboxAppManifest; + checkedFiles: readonly string[]; + smoke: { + standaloneUrl: string; + contextualUrl: string; + fetchedUrls: readonly string[]; + }; +} + +interface CheckedReference { + reference: string; + path: string; +} + +function decodedReference(reference: string, label: string): string { + let decoded = reference; + try { + for (let count = 0; count < 3; count += 1) { + const next = decodeURIComponent(decoded); + if (next === decoded) break; + decoded = next; + } + } catch (cause) { + throw new ToolboxCheckError(`${label} contains invalid URL encoding`, { + cause, + }); + } + return decoded; +} + +function localReference( + root: string, + reference: string, + label: string, +): CheckedReference { + const decoded = decodedReference(reference, label); + const pathPart = decoded.split(/[?#]/u, 1)[0] ?? ""; + if ( + pathPart.startsWith("/") || + pathPart.includes("\\") || + /^[a-z][a-z\d+.-]*:/iu.test(pathPart) || + pathPart.split("/").includes("..") || + pathPart.includes("\0") + ) { + throw new ToolboxCheckError(`${label} uses an unsafe path: ${reference}`); + } + + let filePath = resolve(root, pathPart || "."); + if (pathPart.endsWith("/") || pathPart === "." || pathPart === "./") { + filePath = resolve(filePath, "index.html"); + } + const fromRoot = relative(root, filePath); + if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`)) { + throw new ToolboxCheckError(`${label} escapes the distribution directory`); + } + return { reference, path: filePath }; +} + +async function assertRegularContainedFile( + root: string, + item: CheckedReference, + label: string, +): Promise { + try { + await lstat(item.path); + } catch (cause) { + throw new ToolboxCheckError(`${label} does not exist: ${item.reference}`, { + cause, + }); + } + const [realRoot, realFile] = await Promise.all([ + realpath(root), + realpath(item.path), + ]); + const fromRealRoot = relative(realRoot, realFile); + if (fromRealRoot === ".." || fromRealRoot.startsWith(`..${sep}`)) { + throw new ToolboxCheckError(`${label} resolves outside the distribution`); + } + const info = await stat(realFile); + if (!info.isFile()) { + throw new ToolboxCheckError(`${label} is not a file: ${item.reference}`); + } + return item.path; +} + +function contentType(path: string): string { + switch (extname(path).toLowerCase()) { + case ".html": + return "text/html; charset=utf-8"; + case ".json": + return "application/json; charset=utf-8"; + case ".css": + return "text/css; charset=utf-8"; + case ".js": + case ".mjs": + return "text/javascript; charset=utf-8"; + case ".svg": + return "image/svg+xml"; + case ".png": + return "image/png"; + default: + return "application/octet-stream"; + } +} + +function encodedPath(path: string): string { + return path.split("/").map(encodeURIComponent).join("/"); +} + +function catalogDocument(manifestName: string): object { + return { + schemaVersion: 1, + id: "de.add-ideas.toolbox-check", + name: "Toolbox check", + home: "/", + theme: { mode: "system", brand: "Toolbox check" }, + apps: [ + { + manifest: `${DEEP_PREFIX}${encodedPath(manifestName)}`, + enabled: true, + }, + ], + }; +} + +function staticServer(root: string, manifestName: string): Server { + return createServer((request, response) => { + void (async () => { + const requestUrl = new URL(request.url ?? "/", "http://localhost"); + if (requestUrl.pathname === CATALOG_PATH) { + response.writeHead(200, { + "content-type": "application/json; charset=utf-8", + }); + response.end(JSON.stringify(catalogDocument(manifestName))); + return; + } + if (!requestUrl.pathname.startsWith(DEEP_PREFIX)) { + response.writeHead(404).end("Not found"); + return; + } + let relativePath: string; + try { + relativePath = decodeURIComponent( + requestUrl.pathname.slice(DEEP_PREFIX.length), + ); + } catch { + response.writeHead(400).end("Bad path"); + return; + } + const checked = localReference( + root, + relativePath || "./", + "Request path", + ); + try { + await assertRegularContainedFile(root, checked, "Requested file"); + } catch { + response.writeHead(404).end("Not found"); + return; + } + response.writeHead(200, { "content-type": contentType(checked.path) }); + createReadStream(checked.path).pipe(response); + })().catch(() => { + if (!response.headersSent) response.writeHead(500); + response.end("Server error"); + }); + }); +} + +async function listen(server: Server): Promise { + await new Promise((resolvePromise, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => resolvePromise()); + }); + const address = server.address(); + if (address === null || typeof address === "string") { + throw new ToolboxCheckError("Could not determine smoke-test server port"); + } + return address.port; +} + +async function close(server: Server): Promise { + await new Promise((resolvePromise, reject) => { + server.close((error) => (error ? reject(error) : resolvePromise())); + }); +} + +async function requireOk(url: URL, label: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new ToolboxCheckError(`${label} returned HTTP ${response.status}`); + } + return response; +} + +interface HtmlResource { + kind: "icon" | "image" | "manifest" | "script" | "stylesheet"; + reference: string; +} + +function attributes(source: string): Map { + const result = new Map(); + const pattern = /([:\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gu; + for (const match of source.matchAll(pattern)) { + const name = match[1]?.toLowerCase(); + const value = match[2] ?? match[3] ?? match[4]; + if (name !== undefined && value !== undefined) result.set(name, value); + } + return result; +} + +function htmlResources(html: string): readonly HtmlResource[] { + const resources: HtmlResource[] = []; + const tagPattern = /<(script|link|img)\b([^>]*)>/giu; + for (const match of html.matchAll(tagPattern)) { + const tag = match[1]?.toLowerCase(); + const values = attributes(match[2] ?? ""); + if (tag === "script") { + const reference = values.get("src"); + if (reference) resources.push({ kind: "script", reference }); + } else if (tag === "img") { + const reference = values.get("src"); + if (reference) resources.push({ kind: "image", reference }); + } else if (tag === "link") { + const reference = values.get("href"); + const relationships = new Set( + (values.get("rel") ?? "").toLowerCase().split(/\s+/u).filter(Boolean), + ); + if (!reference) continue; + if (relationships.has("manifest")) { + resources.push({ kind: "manifest", reference }); + } else if (relationships.has("stylesheet")) { + resources.push({ kind: "stylesheet", reference }); + } else if ( + relationships.has("icon") || + relationships.has("apple-touch-icon") + ) { + resources.push({ kind: "icon", reference }); + } else if ( + relationships.has("modulepreload") || + relationships.has("preload") + ) { + resources.push({ kind: "script", reference }); + } + } + } + return resources; +} + +function nestedLocalUrl( + reference: string, + base: URL, + label: string, +): URL | undefined { + let url: URL; + try { + url = new URL(reference, base); + } catch (cause) { + throw new ToolboxCheckError(`${label} has an invalid URL: ${reference}`, { + cause, + }); + } + if (url.protocol !== "http:" && url.protocol !== "https:") return undefined; + if (url.origin !== base.origin) return undefined; + if (!url.pathname.startsWith(DEEP_PREFIX)) { + throw new ToolboxCheckError( + `${label} is not relocatable under the nested prefix: ${reference}`, + ); + } + return url; +} + +async function inspectWebManifest( + response: Response, + manifestUrl: URL, +): Promise { + let document: unknown; + try { + document = await response.json(); + } catch (cause) { + throw new ToolboxCheckError("Linked web manifest is not valid JSON", { + cause, + }); + } + if (typeof document !== "object" || document === null) return []; + const icons = (document as { icons?: unknown }).icons; + if (icons === undefined) return []; + if (!Array.isArray(icons)) { + throw new ToolboxCheckError("Linked web manifest icons must be an array"); + } + const fetched: string[] = []; + for (const [index, icon] of icons.entries()) { + if (typeof icon !== "object" || icon === null) { + throw new ToolboxCheckError( + `Web manifest icon ${index} must be an object`, + ); + } + const source = (icon as { src?: unknown }).src; + if (typeof source !== "string" || source.length === 0) { + throw new ToolboxCheckError(`Web manifest icon ${index} needs a src`); + } + const iconUrl = nestedLocalUrl(source, manifestUrl, "Web manifest icon"); + if (iconUrl !== undefined) { + await requireOk(iconUrl, "Web manifest icon"); + fetched.push(iconUrl.href); + } + } + return fetched; +} + +async function inspectEntryHtml( + html: string, + entryUrl: URL, +): Promise { + const fetched: string[] = []; + for (const resource of htmlResources(html)) { + const label = `HTML ${resource.kind}`; + const url = nestedLocalUrl(resource.reference, entryUrl, label); + if (url === undefined) continue; + const response = await requireOk(url, label); + fetched.push(url.href); + if (resource.kind === "manifest") { + fetched.push(...(await inspectWebManifest(response, url))); + } + } + return fetched; +} + +async function smokeCheck( + root: string, + manifest: ToolboxAppManifest, + manifestName: string, +): Promise { + const server = staticServer(root, manifestName); + const port = await listen(server); + try { + const appBase = new URL(DEEP_PREFIX, `http://127.0.0.1:${port}/`); + const standaloneUrl = new URL(manifest.entry, appBase); + const standaloneResponse = await requireOk( + standaloneUrl, + "Standalone entry", + ); + const fetchedUrls = await inspectEntryHtml( + await standaloneResponse.text(), + standaloneUrl, + ); + const standalone = await loadToolboxContext({ location: standaloneUrl }); + if (standalone.status !== "standalone") { + throw new ToolboxCheckError( + "The standalone smoke URL discovered a toolbox", + ); + } + + const contextualUrl = new URL(standaloneUrl); + contextualUrl.searchParams.set("toolbox", CATALOG_PATH); + await requireOk(contextualUrl, "Contextual entry"); + const contextual = await loadToolboxContext({ location: contextualUrl }); + if (contextual.status !== "ready") { + const reason = + contextual.status === "error" ? `: ${contextual.error.message}` : ""; + throw new ToolboxCheckError( + `Toolbox context smoke check failed${reason}`, + ); + } + const loaded = contextual.context.catalog.apps.find( + (entry) => entry.kind === "manifest", + ); + if (loaded?.kind !== "manifest" || loaded.app.manifest.id !== manifest.id) { + throw new ToolboxCheckError( + "Smoke catalog did not resolve the built app", + ); + } + return { + standaloneUrl: standaloneUrl.href, + contextualUrl: contextualUrl.href, + fetchedUrls, + }; + } finally { + await close(server); + } +} + +export async function checkToolboxDist( + dist: string, + options: ToolboxCheckOptions = {}, +): Promise { + const root = resolve(dist); + let rootInfo; + try { + rootInfo = await stat(root); + } catch (cause) { + throw new ToolboxCheckError( + `Distribution directory does not exist: ${root}`, + { + cause, + }, + ); + } + if (!rootInfo.isDirectory()) { + throw new ToolboxCheckError( + `Distribution path is not a directory: ${root}`, + ); + } + + const manifestName = options.manifestName ?? "toolbox-app.json"; + const manifestReference = localReference(root, manifestName, "Manifest"); + const manifestPath = await assertRegularContainedFile( + root, + manifestReference, + "Manifest", + ); + let document: unknown; + try { + document = JSON.parse(await readFile(manifestPath, "utf8")); + } catch (cause) { + throw new ToolboxCheckError(`Manifest is not valid JSON: ${manifestPath}`, { + cause, + }); + } + let app: ToolboxAppManifest; + try { + app = parseToolboxApp(document); + } catch (cause) { + throw new ToolboxCheckError("Manifest does not satisfy toolbox app v1", { + cause, + }); + } + if (!SEMVER_PATTERN.test(app.version)) { + throw new ToolboxCheckError( + `Manifest version is not SemVer: ${app.version}`, + ); + } + + const references: Array<[string, string]> = [ + [app.entry, "Application entry"], + [app.icon, "Application icon"], + ...(app.assets ?? []).map((asset): [string, string] => [ + asset, + "Application asset", + ]), + ]; + const checkedFiles = await Promise.all( + references.map(([reference, label]) => + assertRegularContainedFile( + root, + localReference(root, reference, label), + label, + ), + ), + ); + const smoke = await smokeCheck(root, app, manifestName); + return { root, manifestPath, app, checkedFiles, smoke }; +} diff --git a/packages/testkit/src/cli.ts b/packages/testkit/src/cli.ts new file mode 100644 index 0000000..0d0e23c --- /dev/null +++ b/packages/testkit/src/cli.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { checkToolboxDist } from "./check.js"; + +function usage(): void { + console.log("Usage: toolbox-check "); +} + +const arguments_ = process.argv.slice(2); +if (arguments_.includes("--help") || arguments_.includes("-h")) { + usage(); +} else if (arguments_.length !== 1) { + usage(); + process.exitCode = 2; +} else { + checkToolboxDist(arguments_[0] as string) + .then((report) => { + console.log( + `toolbox-check: ${report.app.id}@${report.app.version} passed (${report.checkedFiles.length} files, standalone + contextual smoke checks)`, + ); + }) + .catch((error: unknown) => { + console.error( + `toolbox-check: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exitCode = 1; + }); +} diff --git a/packages/testkit/src/index.ts b/packages/testkit/src/index.ts new file mode 100644 index 0000000..0d91bae --- /dev/null +++ b/packages/testkit/src/index.ts @@ -0,0 +1,2 @@ +export { checkToolboxDist, ToolboxCheckError } from "./check.js"; +export type { ToolboxCheckOptions, ToolboxCheckReport } from "./check.js"; diff --git a/packages/testkit/test/check.test.ts b/packages/testkit/test/check.test.ts new file mode 100644 index 0000000..df0bfc5 --- /dev/null +++ b/packages/testkit/test/check.test.ts @@ -0,0 +1,118 @@ +// @vitest-environment node + +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { checkToolboxDist } from "../src/index.js"; + +const temporaryDirectories: string[] = []; + +const manifest = (overrides: Record = {}) => ({ + schemaVersion: 1, + id: "de.add-ideas.example", + name: "Example", + version: "1.2.3", + description: "Example app", + entry: "./", + icon: "./icon.svg", + categories: ["example"], + tags: ["test"], + integration: { + contextVersion: 1, + launchModes: ["navigate"], + embedding: "unsupported", + }, + requirements: { + secureContext: false, + workers: false, + indexedDb: false, + crossOriginIsolated: false, + }, + privacy: { + processing: "local", + fileUploads: false, + telemetry: false, + }, + source: { + repository: "https://git.example.test/example", + license: "MIT", + }, + assets: ["./app.js"], + ...overrides, +}); + +async function fixture(document = manifest()): Promise { + const root = await mkdtemp(join(tmpdir(), "toolbox-check-")); + temporaryDirectories.push(root); + await Promise.all([ + writeFile(join(root, "toolbox-app.json"), JSON.stringify(document)), + writeFile( + join(root, "index.html"), + 'Example', + ), + writeFile( + join(root, "icon.svg"), + '', + ), + writeFile(join(root, "app.js"), "console.log('ok')"), + writeFile(join(root, "style.css"), "body { color: black; }"), + writeFile(join(root, "image.png"), "not-a-real-png"), + writeFile( + join(root, "site.webmanifest"), + JSON.stringify({ icons: [{ src: "./icon.svg" }] }), + ), + ]); + return root; +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((path) => rm(path, { recursive: true, force: true })), + ); +}); + +describe("toolbox-check", () => { + it("validates files and smoke-fetches nested standalone and context URLs", async () => { + const report = await checkToolboxDist(await fixture()); + + expect(report.app.id).toBe("de.add-ideas.example"); + expect(report.checkedFiles).toHaveLength(3); + expect(new URL(report.smoke.standaloneUrl).pathname).toBe( + "/__toolbox-check__/deep/nested/app/", + ); + expect( + new URL(report.smoke.contextualUrl).searchParams.get("toolbox"), + ).toBe("/toolbox.catalog.json"); + expect(report.smoke.fetchedUrls).toEqual( + expect.arrayContaining([ + expect.stringContaining("/site.webmanifest"), + expect.stringContaining("/style.css"), + expect.stringContaining("/app.js"), + ]), + ); + }); + + it("rejects traversal in declared assets", async () => { + const root = await fixture(manifest({ assets: ["../secret.txt"] })); + await expect(checkToolboxDist(root)).rejects.toThrow(/unsafe path/u); + }); + + it("rejects a non-SemVer manifest version", async () => { + const root = await fixture(manifest({ version: "next" })); + await expect(checkToolboxDist(root)).rejects.toThrow(/not SemVer/u); + }); + + it("rejects root-absolute HTML assets that break nested deployment", async () => { + const root = await fixture(); + await writeFile( + join(root, "index.html"), + '', + ); + await expect(checkToolboxDist(root)).rejects.toThrow(/not relocatable/u); + }); +}); diff --git a/packages/testkit/tsconfig.build.json b/packages/testkit/tsconfig.build.json new file mode 100644 index 0000000..bb07ae1 --- /dev/null +++ b/packages/testkit/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/testkit/tsconfig.json b/packages/testkit/tsconfig.json new file mode 100644 index 0000000..bef7333 --- /dev/null +++ b/packages/testkit/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"], + "paths": { + "@add-ideas/toolbox-contract": ["../contract/src/index.ts"] + } + }, + "include": ["src"] +} diff --git a/schemas/toolbox-app.v1.schema.json b/schemas/toolbox-app.v1.schema.json new file mode 100644 index 0000000..5c41bcc --- /dev/null +++ b/schemas/toolbox-app.v1.schema.json @@ -0,0 +1,219 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json", + "title": "Toolbox application manifest v1", + "type": "object", + "required": [ + "schemaVersion", + "id", + "name", + "version", + "description", + "entry", + "icon", + "categories", + "tags", + "integration", + "requirements", + "privacy" + ], + "properties": { + "schemaVersion": { + "const": 1 + }, + "id": { + "type": "string", + "pattern": "^[a-z0-9]+(?:[._-][a-z0-9]+)*$" + }, + "name": { + "$ref": "#/$defs/nonEmptyString" + }, + "version": { + "$ref": "#/$defs/nonEmptyString" + }, + "entry": { + "$ref": "#/$defs/urlReference" + }, + "description": { + "type": "string" + }, + "icon": { + "$ref": "#/$defs/urlReference" + }, + "privacy": { + "type": "object", + "required": ["processing", "fileUploads", "telemetry"], + "properties": { + "processing": { + "enum": ["local", "remote", "mixed"] + }, + "fileUploads": { + "type": "boolean" + }, + "telemetry": { + "type": "boolean" + }, + "label": { + "$ref": "#/$defs/nonEmptyString" + }, + "url": { + "$ref": "#/$defs/urlReference" + } + }, + "additionalProperties": true + }, + "categories": { + "$ref": "#/$defs/stringList" + }, + "tags": { + "$ref": "#/$defs/stringList" + }, + "integration": { + "type": "object", + "required": ["contextVersion", "launchModes", "embedding"], + "properties": { + "contextVersion": { + "const": 1 + }, + "launchModes": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": ["navigate", "new-tab"] + } + }, + "embedding": { + "enum": ["unsupported", "supported"] + } + }, + "additionalProperties": true + }, + "requirements": { + "type": "object", + "required": [ + "secureContext", + "workers", + "indexedDb", + "crossOriginIsolated" + ], + "properties": { + "secureContext": { + "type": "boolean" + }, + "workers": { + "type": "boolean" + }, + "indexedDb": { + "type": "boolean" + }, + "crossOriginIsolated": { + "type": "boolean" + }, + "topLevelContext": { + "type": "boolean" + } + }, + "additionalProperties": true + }, + "source": { + "type": "object", + "required": ["repository", "license"], + "properties": { + "repository": { + "$ref": "#/$defs/absoluteWebUrl" + }, + "license": { + "$ref": "#/$defs/nonEmptyString" + } + }, + "additionalProperties": true + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "label", "url"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9]+(?:[._-][a-z0-9]+)*$" + }, + "label": { + "$ref": "#/$defs/nonEmptyString" + }, + "url": { + "$ref": "#/$defs/urlReference" + } + }, + "additionalProperties": true + } + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/$defs/urlReference" + }, + "uniqueItems": true + } + }, + "$defs": { + "urlReference": { + "type": "string", + "allOf": [ + { + "$ref": "#/$defs/nonEmptyString" + }, + { + "type": "string", + "description": "Backslashes are not URL path separators in toolbox documents.", + "pattern": "^[^\\\\]*$" + }, + { + "type": "string", + "description": "Explicit schemes are limited to HTTP and HTTPS; relative references remain valid.", + "pattern": "^[\\t\\n\\r ]*(?:[Hh][Tt][Tt][Pp][Ss]?:|(?!(?:[A-Za-z][A-Za-z0-9+.-]*):))" + }, + { + "type": "string", + "description": "An explicit HTTP(S) authority must not be empty.", + "pattern": "^(?![\\t\\n\\r ]*[Hh][Tt][Tt][Pp][Ss]?:/+(?:[?#]|$))" + }, + { + "type": "string", + "description": "HTTP(S) authority references must not contain credentials.", + "pattern": "^[\\t\\n\\r ]*(?!(?:[Hh][Tt][Tt][Pp]:/*|[Hh][Tt][Tt][Pp][Ss]:/+|/+)[^/?#]*@)" + } + ] + }, + "absoluteWebUrl": { + "allOf": [ + { + "$ref": "#/$defs/urlReference" + }, + { + "type": "string", + "description": "Repository URLs must be absolute HTTP(S) URLs.", + "pattern": "^[\\t\\n\\r ]*[Hh][Tt][Tt][Pp][Ss]?:/*[^\\s/?#]" + }, + { + "type": "string", + "description": "Absolute repository URLs must not contain credentials.", + "pattern": "^[\\t\\n\\r ]*(?![Hh][Tt][Tt][Pp][Ss]?:/*[^/?#]*@)" + } + ] + }, + "nonEmptyString": { + "type": "string", + "pattern": "\\S" + }, + "stringList": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/nonEmptyString" + } + } + }, + "additionalProperties": true +} diff --git a/schemas/toolbox-catalog.v1.schema.json b/schemas/toolbox-catalog.v1.schema.json new file mode 100644 index 0000000..a74e914 --- /dev/null +++ b/schemas/toolbox-catalog.v1.schema.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json", + "title": "Toolbox catalog v1", + "type": "object", + "required": ["schemaVersion", "id", "name", "home", "theme", "apps"], + "properties": { + "schemaVersion": { + "const": 1 + }, + "id": { + "type": "string", + "pattern": "^[a-z0-9]+(?:[._-][a-z0-9]+)*$" + }, + "name": { + "$ref": "#/$defs/nonEmptyString" + }, + "home": { + "$ref": "#/$defs/urlReference" + }, + "theme": { + "type": "object", + "required": ["mode", "brand"], + "properties": { + "mode": { + "enum": ["light", "dark", "system"] + }, + "brand": { + "$ref": "#/$defs/nonEmptyString" + } + }, + "additionalProperties": true + }, + "apps": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "required": ["manifest", "enabled"], + "not": { + "required": ["entry"] + }, + "properties": { + "manifest": { + "$ref": "#/$defs/urlReference" + }, + "enabled": { + "type": "boolean" + } + }, + "additionalProperties": true + }, + { + "type": "object", + "required": ["name", "entry", "launch", "enabled"], + "not": { + "required": ["manifest"] + }, + "properties": { + "name": { + "$ref": "#/$defs/nonEmptyString" + }, + "entry": { + "$ref": "#/$defs/urlReference" + }, + "launch": { + "enum": ["navigate", "new-tab"] + }, + "enabled": { + "type": "boolean" + } + }, + "additionalProperties": true + } + ] + } + } + }, + "$defs": { + "urlReference": { + "type": "string", + "allOf": [ + { + "$ref": "#/$defs/nonEmptyString" + }, + { + "type": "string", + "description": "Backslashes are not URL path separators in toolbox documents.", + "pattern": "^[^\\\\]*$" + }, + { + "type": "string", + "description": "Explicit schemes are limited to HTTP and HTTPS; relative references remain valid.", + "pattern": "^[\\t\\n\\r ]*(?:[Hh][Tt][Tt][Pp][Ss]?:|(?!(?:[A-Za-z][A-Za-z0-9+.-]*):))" + }, + { + "type": "string", + "description": "An explicit HTTP(S) authority must not be empty.", + "pattern": "^(?![\\t\\n\\r ]*[Hh][Tt][Tt][Pp][Ss]?:/+(?:[?#]|$))" + }, + { + "type": "string", + "description": "HTTP(S) authority references must not contain credentials.", + "pattern": "^[\\t\\n\\r ]*(?!(?:[Hh][Tt][Tt][Pp]:/*|[Hh][Tt][Tt][Pp][Ss]:/+|/+)[^/?#]*@)" + } + ] + }, + "nonEmptyString": { + "type": "string", + "pattern": "\\S" + } + }, + "additionalProperties": true +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..77b4d6e --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..96ff60d --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,26 @@ +import { fileURLToPath } from "node:url"; + +import { defineConfig } from "vitest/config"; + +const fromRoot = (path: string): string => + fileURLToPath(new URL(path, import.meta.url)); + +export default defineConfig({ + resolve: { + alias: { + "@add-ideas/toolbox-contract": fromRoot( + "./packages/contract/src/index.ts", + ), + "@add-ideas/toolbox-shell-react": fromRoot( + "./packages/shell-react/src/index.ts", + ), + }, + }, + test: { + environment: "jsdom", + setupFiles: [fromRoot("./vitest.setup.ts")], + coverage: { + reporter: ["text", "html"], + }, + }, +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..004761e --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,8 @@ +import "@testing-library/jest-dom/vitest"; + +import { cleanup } from "@testing-library/react"; +import { afterEach } from "vitest"; + +afterEach(() => { + cleanup(); +});