From ce9bddb218110dd6c13c106668585fe68df4800e Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 20 Jul 2026 17:53:56 +0200 Subject: [PATCH] feat: add toolbox contract shell and testkit --- .gitignore | 5 + .npmrc | 1 + .prettierignore | 4 + .prettierrc.json | 4 + CHANGELOG.md | 21 + LICENSE | 192 + README.md | 231 + eslint.config.js | 31 + examples/react-app/README.md | 15 + examples/react-app/public/icon.svg | 4 + examples/react-app/public/toolbox-app.json | 32 + .../react-app/public/toolbox.catalog.json | 23 + examples/react-app/src/App.tsx | 19 + examples/react-app/src/main.tsx | 13 + examples/react-app/src/toolbox-app.ts | 33 + package-lock.json | 3982 +++++++++++++++++ package.json | 49 + packages/contract/LICENSE | 192 + packages/contract/README.md | 21 + packages/contract/package.json | 48 + packages/contract/src/discovery.ts | 110 + packages/contract/src/index.ts | 60 + packages/contract/src/load.ts | 245 + packages/contract/src/parse.ts | 528 +++ packages/contract/src/types.ts | 178 + packages/contract/src/urls.ts | 131 + packages/contract/test/contract.test.ts | 288 ++ packages/contract/test/schema-parity.test.ts | 261 ++ packages/contract/tsconfig.build.json | 7 + packages/contract/tsconfig.json | 8 + packages/shell-react/LICENSE | 192 + packages/shell-react/README.md | 15 + packages/shell-react/package.json | 53 + packages/shell-react/src/AppShell.tsx | 231 + packages/shell-react/src/index.ts | 2 + packages/shell-react/src/styles.css | 202 + packages/shell-react/test/AppShell.test.tsx | 202 + packages/shell-react/tsconfig.build.json | 9 + packages/shell-react/tsconfig.json | 11 + packages/testkit/LICENSE | 192 + packages/testkit/README.md | 17 + packages/testkit/package.json | 52 + packages/testkit/src/check.ts | 482 ++ packages/testkit/src/cli.ts | 28 + packages/testkit/src/index.ts | 2 + packages/testkit/test/check.test.ts | 118 + packages/testkit/tsconfig.build.json | 9 + packages/testkit/tsconfig.json | 11 + schemas/toolbox-app.v1.schema.json | 219 + schemas/toolbox-catalog.v1.schema.json | 115 + tsconfig.base.json | 18 + vitest.config.ts | 26 + vitest.setup.ts | 8 + 53 files changed, 8950 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 examples/react-app/README.md create mode 100644 examples/react-app/public/icon.svg create mode 100644 examples/react-app/public/toolbox-app.json create mode 100644 examples/react-app/public/toolbox.catalog.json create mode 100644 examples/react-app/src/App.tsx create mode 100644 examples/react-app/src/main.tsx create mode 100644 examples/react-app/src/toolbox-app.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 packages/contract/LICENSE create mode 100644 packages/contract/README.md create mode 100644 packages/contract/package.json create mode 100644 packages/contract/src/discovery.ts create mode 100644 packages/contract/src/index.ts create mode 100644 packages/contract/src/load.ts create mode 100644 packages/contract/src/parse.ts create mode 100644 packages/contract/src/types.ts create mode 100644 packages/contract/src/urls.ts create mode 100644 packages/contract/test/contract.test.ts create mode 100644 packages/contract/test/schema-parity.test.ts create mode 100644 packages/contract/tsconfig.build.json create mode 100644 packages/contract/tsconfig.json create mode 100644 packages/shell-react/LICENSE create mode 100644 packages/shell-react/README.md create mode 100644 packages/shell-react/package.json create mode 100644 packages/shell-react/src/AppShell.tsx create mode 100644 packages/shell-react/src/index.ts create mode 100644 packages/shell-react/src/styles.css create mode 100644 packages/shell-react/test/AppShell.test.tsx create mode 100644 packages/shell-react/tsconfig.build.json create mode 100644 packages/shell-react/tsconfig.json create mode 100644 packages/testkit/LICENSE create mode 100644 packages/testkit/README.md create mode 100644 packages/testkit/package.json create mode 100644 packages/testkit/src/check.ts create mode 100644 packages/testkit/src/cli.ts create mode 100644 packages/testkit/src/index.ts create mode 100644 packages/testkit/test/check.test.ts create mode 100644 packages/testkit/tsconfig.build.json create mode 100644 packages/testkit/tsconfig.json create mode 100644 schemas/toolbox-app.v1.schema.json create mode 100644 schemas/toolbox-catalog.v1.schema.json create mode 100644 tsconfig.base.json create mode 100644 vitest.config.ts create mode 100644 vitest.setup.ts 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(); +});