diff --git a/.gitea/workflows/release-draft.yml b/.gitea/workflows/release-draft.yml index 88e409f..6954ba7 100644 --- a/.gitea/workflows/release-draft.yml +++ b/.gitea/workflows/release-draft.yml @@ -52,7 +52,7 @@ jobs: uses: https://github.com/actions/cache@v4 with: path: /root/.npm - key: npm-linux-${{ hashFiles('package-lock.json', 'server/package-lock.json') }} + key: npm-linux-${{ hashFiles('package-lock.json', 'server/package-lock.json', 'docs-site/package-lock.json') }} restore-keys: npm-linux- - name: Restore Electron cache @@ -71,6 +71,7 @@ jobs: apt-get update && apt-get install -y --no-install-recommends zip npm ci cd server && npm ci + cd ../docs-site && npm ci - name: Set CI release version run: > @@ -83,6 +84,7 @@ jobs: cd toju-app npx ng build --configuration production --base-href='./' cd .. + npm run build:docs npx --package typescript tsc -p tsconfig.electron.json cd server node ../tools/sync-server-build-version.js @@ -124,7 +126,7 @@ jobs: uses: https://github.com/actions/cache@v4 with: path: ~/AppData/Local/npm-cache - key: npm-windows-${{ hashFiles('package-lock.json', 'server/package-lock.json') }} + key: npm-windows-${{ hashFiles('package-lock.json', 'server/package-lock.json', 'docs-site/package-lock.json') }} restore-keys: npm-windows- - name: Restore Electron cache @@ -142,6 +144,7 @@ jobs: run: | npm ci npm ci --prefix server + npm ci --prefix docs-site - name: Set CI release version run: > @@ -154,6 +157,7 @@ jobs: Push-Location "toju-app" npx ng build --configuration production --base-href='./' Pop-Location + npm run build:docs npx --package typescript tsc -p tsconfig.electron.json Push-Location server node ../tools/sync-server-build-version.js @@ -194,6 +198,7 @@ jobs: Copy-Item -Path (Join-Path $projectRoot 'package.json') -Destination (Join-Path $electronBuilderWorkspace 'package.json') -Force Copy-Item -Path (Join-Path $projectRoot 'package-lock.json') -Destination (Join-Path $electronBuilderWorkspace 'package-lock.json') -Force Invoke-RoboCopy (Join-Path $projectRoot 'dist') (Join-Path $electronBuilderWorkspace 'dist') + Invoke-RoboCopy (Join-Path $projectRoot 'docs-site/build') (Join-Path $electronBuilderWorkspace 'docs-site/build') Invoke-RoboCopy (Join-Path $projectRoot 'images') (Join-Path $electronBuilderWorkspace 'images') Invoke-RoboCopy (Join-Path $projectRoot 'node_modules') (Join-Path $electronBuilderWorkspace 'node_modules') diff --git a/.gitignore b/.gitignore index c81291f..b954335 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ yarn-error.log dist-electron node_modules/* *server/node_modules/* +/docs-site/node_modules/ .angular # IDEs and editors .idea/ @@ -39,6 +40,8 @@ node_modules/* .sass-cache/ /connect.lock /coverage +/docs-site/.docusaurus/ +/docs-site/build/ /libpeerconnection.log testem.log /typings diff --git a/README.md b/README.md index 0ec69b9..4cd99ea 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,15 @@ MetoYou is a desktop-first chat stack managed as an npm monorepo. The repository | `server/` | Signaling server, server-directory API, and websocket runtime | [server/README.md](server/README.md) | | `e2e/` | Playwright end-to-end coverage for the product client | [e2e/README.md](e2e/README.md) | | `website/` | Angular 19 marketing site served separately from the product client | [website/README.md](website/README.md) | +| `docs-site/` | Docusaurus app and plugin documentation served by the Electron Local API | [docs-site/docs/intro.md](docs-site/docs/intro.md) | ## Install 1. Run `npm install` from the repository root. 2. Run `cd server && npm install` for the server package. 3. If you need to work on the marketing site, run `cd website && npm install`. -4. Copy `.env.example` to `.env`. +4. If you need to work on the Docusaurus docs, run `cd docs-site && npm install`. +5. Copy `.env.example` to `.env`. ## Configuration @@ -36,8 +38,9 @@ MetoYou is a desktop-first chat stack managed as an npm monorepo. The repository - `npm run electron:dev` starts the Angular product client and Electron together. - `npm run server:dev` starts only the server with reload. - `npm run build` builds the Angular product client to `dist/client`. +- `npm run build:docs` builds the Docusaurus documentation site to `docs-site/build`. - `npm run build:electron` builds the Electron code to `dist/electron`. -- `npm run build:all` builds the product client, Electron, and server. +- `npm run build:all` builds the product client, Docusaurus docs, Electron, and server. - `npm run test` runs the product-client Vitest suite. - `npm run lint` runs ESLint across the repo. - `npm run lint:fix` formats Angular templates, sorts template properties, and applies ESLint fixes. @@ -54,6 +57,7 @@ MetoYou is a desktop-first chat stack managed as an npm monorepo. The repository | `server/src/` | Express app, websocket runtime, config, CQRS, and persistence layers | | `e2e/` | Playwright tests, helpers, fixtures, and page objects | | `website/src/` | Marketing-site pages, assets, and SSR entry points | +| `docs-site/` | Docusaurus source for Electron-hosted application and plugin documentation | | `tools/` | Build, release, formatting, and packaging scripts | ## Product Client Docs diff --git a/docs-site/docs/desktop-and-local-api.md b/docs-site/docs/desktop-and-local-api.md new file mode 100644 index 0000000..f818099 --- /dev/null +++ b/docs-site/docs/desktop-and-local-api.md @@ -0,0 +1,75 @@ +--- +sidebar_position: 3 +--- + +# Desktop and Local API + +## Electron Hosting Model + +The desktop app hosts local documentation through the existing Electron Local API server. This server is implemented with Node's `http` module in the Electron main process and uses async request handlers for routing, file reads, and streamed responses. + +The endpoint is manually activated. Opening the Docusaurus docs from the desktop title bar enables the local server and docs endpoint if necessary, then opens the system browser to the generated static site. + +This avoids: + +- starting a Docusaurus development server inside Electron; +- blocking the renderer thread; +- serving docs from a remote host; +- exposing the endpoint unless the user chooses to activate it. + +## Local Server Settings + +| Setting | Default | Meaning | +| --- | --- | --- | +| `enabled` | `false` | Starts or stops the local HTTP server. | +| `port` | `17878` | Listening port. | +| `exposeOnLan` | `false` | Uses `127.0.0.1` by default; when true, binds to `0.0.0.0`. | +| `scalarEnabled` | `false` | Enables `/docs` for the Scalar OpenAPI reference. | +| `docusaurusEnabled` | `false` | Enables `/docusaurus` for the built Docusaurus documentation. | +| `allowedSignalingServers` | `[]` | Server URLs allowed for Local API login. | + +## Routes + +| Endpoint | Purpose | Auth | +| --- | --- | --- | +| `GET /api/health` | Liveness, app version, timestamp, and LAN exposure status. | No | +| `GET /api/openapi.json` | OpenAPI 3.1 document for local automation clients. | No | +| `GET /docs` | Scalar API reference when Scalar docs are enabled. | No | +| `GET /docusaurus` | Docusaurus documentation entrypoint when Docusaurus docs are enabled. | No | +| `GET /docusaurus/*` | Static Docusaurus assets and pages. | No | +| `POST /api/auth/login` | Exchanges username, password, and allowed signaling server URL for a local bearer token. | No | +| `POST /api/auth/logout` | Revokes the current local bearer token. | Bearer | +| `GET /api/profile` | Reads the current local user profile. | Bearer | +| `GET /api/rooms` | Lists rooms known to this device. | Bearer | +| `GET /api/rooms/{roomId}/messages` | Reads local room messages with `limit` and `offset`. | Bearer | + +## Authentication Flow + +1. Add trusted signaling server URLs in desktop settings. +2. Start the Local API server. +3. Call `POST /api/auth/login` with `username`, `password`, and `serverUrl`. +4. MetoYou validates credentials through the signaling server. +5. The desktop app issues an opaque local bearer token. +6. Use `Authorization: Bearer ` for protected routes. + +Bearer tokens are local to the running desktop app and are cleared when the Local API server stops. + +## Static Documentation Build + +Docusaurus is a static site generator. The repo builds `docs-site/` into `docs-site/build/`, and Electron serves those files from the local API server. + +Development commands: + +```bash +cd docs-site +npm install +npm run start +``` + +Build command: + +```bash +npm run build:docs +``` + +Packaged desktop builds include the generated static output as an Electron extra resource. diff --git a/docs-site/docs/developer/contributing.md b/docs-site/docs/developer/contributing.md new file mode 100644 index 0000000..418bca4 --- /dev/null +++ b/docs-site/docs/developer/contributing.md @@ -0,0 +1,87 @@ +--- +sidebar_position: 1 +--- + +# Contributing + +MetoYou is an npm-managed monorepo. + +## Packages + +| Path | Purpose | +| --- | --- | +| `toju-app/` | Angular renderer, chat client, voice UI, plugin runtime. | +| `electron/` | Electron main process, preload bridge, local database, local REST API, docs host. | +| `server/` | Node/TypeScript signaling server and server-directory HTTP API. | +| `website/` | Angular marketing site. | +| `docs-site/` | Docusaurus documentation site. | +| `e2e/` | Playwright browser and WebRTC tests. | + +## Setup + +Install root dependencies: + +```bash +npm install +``` + +Install server dependencies when working on the signaling server: + +```bash +cd server +npm install +``` + +## Development Commands + +From the repository root: + +```bash +npm run dev +``` + +Useful focused commands: + +```bash +npm run build +npm run build:electron +npm run build:docs +npm run server:build +npm run lint +npm run test +npm run test:e2e -- tests/chat-dm-flow.spec.ts +``` + +Run the Docusaurus dev server: + +```bash +cd docs-site +npm install +npm run start +``` + +Build static docs for Electron packaging: + +```bash +npm run build:docs +``` + +## Repository Rules + +- Keep changes inside the package that owns the behavior. +- Do not edit generated output in `dist/`, `dist-electron/`, `dist-server/`, `server/dist/`, `.angular/`, or `node_modules/`. +- Renderer-facing Electron capabilities must stay aligned across implementation, preload, and renderer bridge types. +- Signal-server plugin support stores metadata only. Plugin execution belongs to the client runtime. +- Update this documentation when user workflows, plugin APIs, REST routes, DOM structure, or development commands change. + +## Documentation Checklist + +When you change a related area, update these pages: + +| Change | Docs to check | +| --- | --- | +| Voice UI or settings | User Guide: Voice Channels and Calls, Developer Guide: App Pages and DOM Structure. | +| Text channels, messages, DMs | User Guide: Text and Direct Messages, plugin message API pages. | +| Plugin manifest/API/runtime | Plugin Development pages and LLM Plugin Builder Guide. | +| Local REST API routes or schemas | Developer Guide: Local REST API and `electron/api/openapi.ts`. | +| Docusaurus hosting | Developer Guide: Docusaurus Site and Desktop and Local API. | \ No newline at end of file diff --git a/docs-site/docs/developer/docusaurus-site.md b/docs-site/docs/developer/docusaurus-site.md new file mode 100644 index 0000000..8c00b17 --- /dev/null +++ b/docs-site/docs/developer/docusaurus-site.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 2 +--- + +# Docusaurus Site + +The Docusaurus documentation lives in `docs-site/` and builds to static files in `docs-site/build/`. + +## Structure + +```text +docs-site/ + docusaurus.config.ts + sidebars.ts + docs/ + intro.md + user-guide/ + developer/ + plugin-development/ + src/css/custom.css +``` + +## Development + +Use the Docusaurus development server while writing docs: + +```bash +cd docs-site +npm run start +``` + +Build the static site: + +```bash +npm run build +``` + +From the repo root, use: + +```bash +npm run build:docs +``` + +## Electron Hosting + +Electron serves the built site through the local API server when Docusaurus docs are enabled. + +| Route | Purpose | +| --- | --- | +| `/docusaurus` | Docusaurus entrypoint. | +| `/docusaurus/*` | Static Docusaurus assets and generated pages. | + +The endpoint is off until the user opens documentation from the desktop app or enables it through local API settings. Electron serves static files only; it does not run `docusaurus start`. + +## Sidebar Rules + +Navigation is controlled by `docs-site/sidebars.ts`. Add every new page there unless it is intentionally hidden. Use categories for larger sections so non-technical users can find the user guide separately from developer material. + +## Content Rules + +- User docs should avoid implementation jargon. +- Developer docs should name exact files, commands, routes, capabilities, and data shapes. +- Plugin API examples should use literal sample input data. +- REST docs should stay aligned with `electron/api/openapi.ts` and `electron/api/router.ts`. +- DOM docs should stay aligned with Angular routes and component selectors. \ No newline at end of file diff --git a/docs-site/docs/developer/dom-structure.md b/docs-site/docs/developer/dom-structure.md new file mode 100644 index 0000000..ecfca9c --- /dev/null +++ b/docs-site/docs/developer/dom-structure.md @@ -0,0 +1,145 @@ +--- +sidebar_position: 3 +--- + +# App Pages and DOM Structure + +This page maps the app routes and important DOM areas. It is useful for plugin authors, testers, and contributors who need stable mental models of where UI mounts. + +## Angular Routes + +| Route | Component | Purpose | +| --- | --- | --- | +| `/` | Redirect | Redirects to `/search`. | +| `/login` | `LoginComponent` | User login. | +| `/register` | `RegisterComponent` | User registration. | +| `/invite/:inviteId` | `InviteComponent` | Resolve and accept invite links. | +| `/search` | `ServerSearchComponent` | Search and join servers. | +| `/room/:roomId` | `ChatRoomComponent` | Main server page with text, voice, members, and plugin panels. | +| `/dm` | `DmWorkspaceComponent` | Direct-message workspace. | +| `/dm/:conversationId` | `DmWorkspaceComponent` | A selected direct-message conversation. | +| `/settings` | `SettingsComponent` | App, voice, server, plugin, desktop, theme, local API settings. | +| `/plugin-store` | `PluginStoreComponent` | Browse plugin sources and install/update plugins. | +| `/plugins/:pluginId/:pageId` | `PluginPageHostComponent` | Host for plugin app pages registered with `api.ui.registerAppPage()`. | + +## Page Shell + +The renderer is an Angular app. The common shell contains router outlet content plus persistent app surfaces such as the server rail, title bar integrations, settings modals, and floating voice controls. + +High-level structure: + +```html + + + + +``` + +## Server Page DOM + +The server page is the most important page for plugins. + +```html + + + +
Text Channels
+
Voice Channels
+
+ +
+
Members
+
+
+ + + + + + + +
+
+``` + +## Text Channel Area + +Text channel UI is owned by the chat domain. + +```html + + + + + + + + +``` + +Plugin touchpoints: + +- `api.ui.registerComposerAction()` adds composer actions. +- `api.ui.registerEmbedRenderer()` renders declared custom embed payloads. +- `api.ui.mountElement()` can mount into a selector such as `app-chat-messages` when the plugin has `ui.dom`. + +## Voice Area + +Voice UI is split between channel membership, controls, and media workspace. + +```html + +
Voice Channels
+
+ + + + + +``` + +Plugin touchpoints: + +- `api.media.playAudioClip()` plays local audio. +- `api.media.addCustomAudioStream()` contributes audio to voice handling. +- `api.media.addCustomVideoStream()` contributes a video stream. +- `api.channels.addAudioChannel()` creates a voice channel entry when the plugin has channel management rights. + +## Plugin Store and Manager DOM + +```html + + + + + + + +``` + +Plugin pages registered through `api.ui.registerAppPage()` render at `/plugins/:pluginId/:pageId`: + +```html + + + +``` + +## Plugin Render Host + +`PluginRenderHostComponent` accepts plugin render functions that return either an `HTMLElement` or a string. Returning an `HTMLElement` is preferred for interactive UI. Returned strings are rendered as simple text content. + +## Stable Selectors for Tests and Plugins + +Prefer plugin APIs over DOM selectors. When direct DOM mounting is necessary, use stable app selectors and keep cleanup through the returned disposable. + +Common targets: + +| Selector | Area | +| --- | --- | +| `body` | Global overlays or modals. | +| `app-chat-messages` | Main text channel surface. | +| `app-rooms-side-panel` | Server side panel. | +| `[data-testid="plugin-room-side-panel"]` | Plugin side-panel area in the server sidebar. | + +Avoid depending on Tailwind utility classes; they are layout details and may change. \ No newline at end of file diff --git a/docs-site/docs/developer/llm-plugin-builder-guide.md b/docs-site/docs/developer/llm-plugin-builder-guide.md new file mode 100644 index 0000000..c971b31 --- /dev/null +++ b/docs-site/docs/developer/llm-plugin-builder-guide.md @@ -0,0 +1,1432 @@ +--- +sidebar_position: 5 +--- + +# LLM Plugin Builder Guide + +Copy this page into an LLM prompt when you want it to build a MetoYou plugin. It is intentionally explicit about the app, communication model, visual structure, manifest format, runtime rules, API types, and examples so the model has fewer gaps to invent around. + +## Task For The LLM + +Build a MetoYou client plugin: a browser-safe JavaScript ES module with a `toju-plugin.json` manifest, loaded by the Angular renderer, running inside the user's local MetoYou app, using only browser APIs and the provided `TojuClientPluginApi`. + +Return a plugin folder like this: + +```text +my-plugin/ + toju-plugin.json + main.js + README.md + icon.svg +``` + +## Hard Rules + +- Do not modify MetoYou core unless the user explicitly asks for a core code change. +- Use plain browser ESM in `main.js`. Do not use Node APIs, `require`, `fs`, `path`, `child_process`, or build tooling unless explicitly requested. +- Use `toju-plugin.json` as the manifest name. +- Put every disposable returned by plugin APIs in `context.subscriptions`. +- Request only capabilities used by the code. +- Do not call moderation, delete, kick, ban, role, channel, server-setting, or other destructive APIs during `activate`. Put them behind visible user action and confirmation. +- Prefer `api.ui.*` extension points. Use `api.ui.mountElement` only when there is no suitable contribution API. +- Do not use `api.ui.mountElement` to add content to the server plugin sidebar. Use `api.ui.registerSidePanel` instead. +- Do not assume route-specific DOM such as `app-chat-messages`, `app-rooms-side-panel`, or `[data-testid="plugin-room-side-panel"]` exists during `activate`. Those elements exist only when the user is on the matching route and Angular has rendered that view. +- `serverData` is local per-user/per-server client data. It is not arbitrary remote server storage. +- Server-installed plugins are requirement metadata plus local client downloads. The signaling server never executes plugin entrypoints. +- Every event used with `api.events.*` must be declared in the manifest `events` array. + +## What MetoYou Is + +MetoYou is a Discord-like chat and voice app: + +- `toju-app/`: Angular renderer and plugin runtime. +- `electron/`: Electron desktop shell, preload bridge, local database, local REST API, local docs host. +- `server/`: Node/TypeScript signaling and directory server. +- `docs-site/`: Docusaurus documentation. + +Users join chat servers. A server has text channels, voice channels, members, roles, permissions, messages, attachments, voice state, screen share state, camera state, presence, and optional server plugin requirements. Users can also use direct messages, but the plugin API is primarily shaped around the current server workspace. + +## Communication Model + +There are three communication boundaries a plugin author must understand: + +```text +1. Signaling plane + Angular renderer <-> WebSocket signaling server + Used for identity, joining servers, presence, typing, plugin requirements, + server-relayed plugin events, WebRTC offers, answers, and ICE candidates. + +2. Peer plane + Angular renderer <-> WebRTC peer connections <-> other clients + Used for media and data-channel events: chat messages, message sync, + attachments, voice state, screen/camera state, and plugin message bus data. + +3. Desktop/local plane + Angular renderer <-> Electron preload bridge <-> Electron main process + Used for local SQLite, local files, cached plugin bundles, local REST API, + local Docusaurus docs hosting, and desktop-only features. +``` + +Plugins run only in the renderer. They do not run in Electron main and do not run on the signaling server. + +Choose communication APIs like this: + +| Need | Use | Notes | +| --- | --- | --- | +| Visible normal chat message | `api.messages.send` | Persists locally, updates chat UI, broadcasts peer chat event. | +| Visible bot-style message | `api.server.registerPluginUser` plus `api.messages.sendAsPluginUser` | Requires `users.manage` and `messages.send`. | +| Plugin state sync between connected clients | `api.messageBus.publish` and `api.messageBus.subscribe` | P2P data-channel envelope, not a visible chat message. | +| Plugin state sync plus recent chat snapshot | `api.messageBus.publish` with `includeLatestMessages` | Also needs `messages.read`. | +| Metadata through signaling server | `api.events.publishServer` and `api.events.subscribeServer` | Event must be declared in manifest. | +| Low-level peer data | `api.p2p.broadcastData` or `api.p2p.sendData` | Prefer message bus for structured topics/subscriptions. | +| Local user preferences | `api.clientData` | User-scoped local storage/database. | +| Local per-server plugin data | `api.serverData` | User-scoped and current-server-scoped local storage/database. | +| App UI extension | `api.ui.*` | Prefer registered contributions over DOM mounting. | +| Audio/video/voice effects | `api.media.*` | Browser media APIs and voice facade. | + +## How The App Looks + +The main app is a dense chat workspace. The most important plugin context is `/room/:roomId`. + +```html + + + + +``` + +Main server page shape: + +```html + + + +
Text Channels
+
Voice Channels
+
+ +
+
Members
+
+
+ + + + + + + +
+
+``` + +Important routes: + +| Route | Purpose | +| --- | --- | +| `/search` | Search and join servers. | +| `/room/:roomId` | Main server workspace with text, voice, members, and plugin panels. | +| `/dm` and `/dm/:conversationId` | Direct-message workspace. | +| `/settings` | App, voice, server, plugin, desktop, theme, and local API settings. | +| `/plugin-store` | Browse and install plugins. | +| `/plugins/:pluginId/:pageId` | Host for pages registered with `api.ui.registerAppPage`. | + +Direct DOM mounting is a last resort. Route-specific targets may not exist when `activate` runs. If `api.ui.mountElement` cannot find the target, it throws `Plugin mount target not found: ` and plugin activation fails. + +Stable direct-mount targets when necessary: + +| Selector | Area | +| --- | --- | +| `body` | Safest global target for overlays, badges, and modals. It exists during activation. | +| `app-chat-messages` | Main text channel surface. Use only after checking the element exists. | +| `app-rooms-side-panel` | Server side panel. Use only after checking the element exists. Prefer `registerSidePanel` for plugin sidebar content. | + +Do not mount directly into `[data-testid="plugin-room-side-panel"]`. That area is owned by the plugin side-panel registry and is rendered only on the server page. For server sidebar UI, use: + +```js +context.subscriptions.push(api.ui.registerSidePanel('control-panel', { + label: 'Control Panel', + order: 20, + render: () => { + const root = document.createElement('section'); + const button = document.createElement('button'); + + button.type = 'button'; + button.textContent = 'Run Action'; + button.addEventListener('click', () => { + api.logger.info('Side-panel action clicked'); + }); + + root.append(button); + return root; + } +})); +``` + +Do not depend on Tailwind classes or internal styling classes. + +## Manifest + +Minimal manifest: + +```json +{ + "schemaVersion": 1, + "id": "example.my-plugin", + "title": "My Plugin", + "description": "Adds a focused MetoYou feature.", + "version": "1.0.0", + "kind": "client", + "scope": "client", + "apiVersion": "1.0.0", + "compatibility": { + "minimumTojuVersion": "1.0.0" + }, + "entrypoint": "./main.js", + "capabilities": ["ui.pages"] +} +``` + +Manifest type: + +```ts +type TojuPluginInstallScope = 'client' | 'server'; +type PluginEventDirection = 'clientToServer' | 'serverRelay' | 'p2pHint'; +type PluginEventScope = 'server' | 'channel' | 'user' | 'plugin'; + +type PluginCapabilityId = + | 'profile.read' + | 'profile.write' + | 'users.read' + | 'users.manage' + | 'roles.read' + | 'roles.manage' + | 'messages.read' + | 'messages.send' + | 'messages.editOwn' + | 'messages.deleteOwn' + | 'messages.moderate' + | 'messages.sync' + | 'channels.read' + | 'channels.manage' + | 'server.read' + | 'server.manage' + | 'p2p.data' + | 'p2p.media' + | 'media.playAudio' + | 'media.addAudioStream' + | 'media.addVideoStream' + | 'audio.volume' + | 'audio.effects' + | 'ui.settings' + | 'ui.pages' + | 'ui.sidePanel' + | 'ui.channelsSection' + | 'ui.embeds' + | 'ui.dom' + | 'storage.local' + | 'storage.serverData.read' + | 'storage.serverData.write' + | 'events.server.publish' + | 'events.server.subscribe' + | 'events.p2p.publish' + | 'events.p2p.subscribe'; + +interface TojuPluginManifest { + schemaVersion: 1; + id: string; + title: string; + description: string; + version: string; + kind: 'client' | 'library'; + scope?: TojuPluginInstallScope; + apiVersion: string; + compatibility: { + minimumTojuVersion: string; + maximumTojuVersion?: string; + verifiedTojuVersion?: string; + }; + entrypoint?: string; + capabilities?: PluginCapabilityId[]; + events?: { + eventName: string; + direction: PluginEventDirection; + scope: PluginEventScope; + maxPayloadBytes?: number; + schema?: string; + }[]; + data?: { + key: string; + scope: string; + storage: 'local' | 'serverData'; + schema?: string; + }[]; + pluginUser?: { + displayName: string; + label?: string; + avatar?: string; + }; + relationships?: { + after?: string[]; + before?: string[]; + conflicts?: string[]; + requires?: { id: string; versionRange?: string }[]; + optional?: { id: string; versionRange?: string }[]; + }; + authors?: { name: string; email?: string; url?: string }[]; + homepage?: string; + bugs?: string; + license?: string; + readme?: string; + changelog?: string; + settings?: Record; + ui?: Record; + bundle?: { url: string; entrypoint?: string }; + load?: { priority?: 'bootstrap' | 'high' | 'default' | 'low' }; +} +``` + +Validation rules: + +- `id` must be lowercase dotted/dashed id style: starts and ends with lowercase letter or number, and may contain lowercase letters, numbers, dots, and hyphens. +- `version` must look like semantic versioning: `1.0.0`, `1.2.3-beta.1`, or `1.2.3+build.4`. +- `schemaVersion` must be `1`. +- `kind` must be `client` or `library`. +- `scope` is optional, `client`, or `server`. +- `compatibility.minimumTojuVersion` is required. +- `kind: "client"` needs `entrypoint`. +- Every capability must be a known `PluginCapabilityId`. +- Every `api.events.*` event must be declared in `events`. + +Scope meanings: + +| Scope | Meaning | +| --- | --- | +| `client` or omitted | Installed globally for this local user/client. | +| `server` | Installed for a specific chat server as local client plugin plus server requirement metadata. | + +Most generated plugins should use `kind: "client"`. Use `kind: "library"` only for dependency metadata with no executable entrypoint. + +## Runtime Lifecycle + +```ts +interface TojuPluginDisposable { + dispose: () => void; +} + +interface TojuPluginActivationContext { + api: TojuClientPluginApi; + manifest: TojuPluginManifest; + pluginId: string; + subscriptions: TojuPluginDisposable[]; +} + +interface TojuClientPluginModule { + activate?: (context: TojuPluginActivationContext) => Promise | void; + ready?: (context: TojuPluginActivationContext) => Promise | void; + deactivate?: (context: TojuPluginActivationContext) => Promise | void; + onPluginDataChanged?: (context: TojuPluginActivationContext, event: unknown) => Promise | void; + onServerRequirementsChanged?: ( + context: TojuPluginActivationContext, + snapshot: PluginRequirementsSnapshot + ) => Promise | void; +} +``` + +Lifecycle behavior: + +- `activate(context)` runs after the plugin module is imported and capability grants are satisfied. +- The runtime passes a frozen `context.api`; never mutate it. +- `ready(context)` runs after the ready-plugin load-order pass. +- `deactivate(context)` runs during unload or reload. +- The host disposes `context.subscriptions` in reverse order after `deactivate`. +- UI contributions are removed by plugin id on unload. +- Activation state is remembered locally. + +Good boilerplate: + +```js +export function activate(context) { + const { api } = context; + + api.logger.info('Activating plugin', { pluginId: context.pluginId }); + + const page = api.ui.registerAppPage('home', { + label: 'My Plugin', + path: '/plugins/example.my-plugin/home', + render: () => { + const root = document.createElement('section'); + const title = document.createElement('h1'); + const button = document.createElement('button'); + const status = document.createElement('p'); + + title.textContent = 'My Plugin'; + button.type = 'button'; + button.textContent = 'Send hello'; + status.textContent = 'Ready.'; + + button.addEventListener('click', () => { + const channelId = api.context.getCurrent().textChannel?.id; + const message = api.messages.send('Hello from My Plugin', channelId); + status.textContent = `Sent message ${message.id}`; + }); + + root.append(title, button, status); + return root; + } + }); + + context.subscriptions.push(page); +} + +export function ready(context) { + context.api.logger.info('Plugin ready'); +} + +export function deactivate(context) { + context.api.logger.info('Plugin deactivating'); +} +``` + +Matching manifest capabilities for that boilerplate: + +```json +{ + "capabilities": ["ui.pages", "messages.send"] +} +``` + +## Shared App Types + +These are the main data shapes returned by plugin APIs. + +```ts +type ChannelType = 'text' | 'voice'; + +interface Channel { + id: string; + name: string; + type: ChannelType; + position: number; +} + +interface Room { + id: string; + name: string; + description?: string; + topic?: string; + hostId: string; + password?: string; + hasPassword?: boolean; + isPrivate: boolean; + createdAt: number; + userCount: number; + maxUsers?: number; + icon?: string; + iconUpdatedAt?: number; + slowModeInterval?: number; + permissions?: RoomPermissions; + channels?: Channel[]; + members?: RoomMember[]; + roles?: RoomRole[]; + roleAssignments?: RoomRoleAssignment[]; + channelPermissions?: ChannelPermissionOverride[]; + sourceId?: string; + sourceName?: string; + sourceUrl?: string; +} + +interface RoomPermissions { + adminsManageRooms?: boolean; + moderatorsManageRooms?: boolean; + adminsManageIcon?: boolean; + moderatorsManageIcon?: boolean; + allowVoice?: boolean; + allowScreenShare?: boolean; + allowFileUploads?: boolean; + slowModeInterval?: number; +} + +type UserStatus = 'online' | 'away' | 'busy' | 'offline' | 'disconnected'; +type UserRole = 'host' | 'admin' | 'moderator' | 'member'; + +interface User { + id: string; + oderId: string; + username: string; + displayName: string; + description?: string; + profileUpdatedAt?: number; + avatarUrl?: string; + avatarHash?: string; + avatarMime?: string; + avatarUpdatedAt?: number; + status: UserStatus; + role: UserRole; + joinedAt: number; + peerId?: string; + isOnline?: boolean; + isAdmin?: boolean; + isRoomOwner?: boolean; + presenceServerIds?: string[]; + voiceState?: VoiceState; + screenShareState?: ScreenShareState; + cameraState?: CameraState; + gameActivity?: unknown; +} + +interface RoomMember { + id: string; + oderId?: string; + username: string; + displayName: string; + description?: string; + profileUpdatedAt?: number; + avatarUrl?: string; + avatarHash?: string; + avatarMime?: string; + avatarUpdatedAt?: number; + role: UserRole; + roleIds?: string[]; + joinedAt: number; + lastSeenAt: number; +} + +interface VoiceState { + isConnected: boolean; + isMuted: boolean; + isDeafened: boolean; + isSpeaking: boolean; + isMutedByAdmin?: boolean; + volume?: number; + roomId?: string; + serverId?: string; +} + +interface Message { + id: string; + roomId: string; + channelId?: string; + senderId: string; + senderName: string; + content: string; + timestamp: number; + editedAt?: number; + reactions: Reaction[]; + isDeleted: boolean; + replyToId?: string; + linkMetadata?: LinkMetadata[]; +} + +interface Reaction { + id: string; + messageId: string; + oderId: string; + userId: string; + emoji: string; + timestamp: number; +} + +interface LinkMetadata { + url: string; + title?: string; + description?: string; + imageUrl?: string; + siteName?: string; + failed?: boolean; +} + +type RoomPermissionKey = + | 'manageServer' + | 'manageRoles' + | 'manageChannels' + | 'manageIcon' + | 'kickMembers' + | 'banMembers' + | 'manageBans' + | 'deleteMessages' + | 'joinVoice' + | 'shareScreen' + | 'uploadFiles'; + +type PermissionState = 'allow' | 'deny' | 'inherit'; +type RoomPermissionMatrix = Partial>; + +interface RoomRole { + id: string; + name: string; + color?: string; + position: number; + isSystem?: boolean; + permissions?: RoomPermissionMatrix; +} + +interface RoomRoleAssignment { + userId: string; + oderId?: string; + roleIds: string[]; +} + +interface ChannelPermissionOverride { + channelId: string; + targetType: 'role' | 'user'; + targetId: string; + permission: RoomPermissionKey; + value: PermissionState; +} +``` + +## Full Plugin API Types + +```ts +interface PluginApiProfileUpdate { displayName: string; description?: string } +interface PluginApiAvatarUpdate { avatarUrl: string; avatarMime: string; avatarHash: string } +interface PluginApiChannelRequest { name: string; id?: string; position?: number } +interface PluginApiServerSettingsUpdate { + name?: string; + description?: string; + topic?: string; + isPrivate?: boolean; + password?: string; + maxUsers?: number; +} +interface PluginApiPluginUserRequest { displayName: string; id?: string; avatarUrl?: string } +interface PluginApiMessageAsPluginUserRequest { pluginUserId: string; content: string; channelId?: string } +interface PluginApiAudioClipRequest { url: string; volume?: number } +interface PluginApiCustomStreamRequest { stream: MediaStream; label?: string } + +type PluginApiActionSource = 'composerAction' | 'toolbarAction' | 'profileAction' | 'manual'; +interface PluginApiActionContext { + source: PluginApiActionSource; + user: User | null; + server: Room | null; + textChannel: Channel | null; + voiceChannel: Channel | null; +} +interface PluginApiTypingEvent extends Omit { + channelId: string; + displayName: string; + isTyping: boolean; + serverId: string; + userId: string; +} +interface PluginEventEnvelope { + type: 'plugin_event'; + pluginId: string; + serverId: string; + eventName: string; + payload: TPayload; + eventId?: string; + emittedAt?: number; + sourceUserId?: string; + sourcePluginUserId?: string; +} +interface PluginApiEventSubscription { + eventName: string; + handler: (event: PluginEventEnvelope) => void; +} + +interface PluginApiMessageBusEnvelope { + eventId: string; + pluginId: string; + roomId: string; + sentAt: number; + topic: string; + channelId?: string; + payload?: unknown; + messages?: Message[]; + sourcePeerId?: string; + sourceUserId?: string; +} +interface PluginApiMessageBusLatestRequest { + targetPeerId?: string; + channelId?: string; + topic?: string; + limit?: number; + sinceTimestamp?: number; + includeDeleted?: boolean; +} +interface PluginApiMessageBusPublishRequest extends PluginApiMessageBusLatestRequest { + topic: string; + payload?: unknown; + includeLatestMessages?: boolean; + includeSelf?: boolean; +} +interface PluginApiMessageBusSubscription { + topic?: string; + channelId?: string; + replayLatest?: boolean; + latestMessageLimit?: number; + handler: (event: PluginApiMessageBusEnvelope) => void; +} + +interface PluginApiPageContribution { label: string; path: string; render: () => HTMLElement | string } +interface PluginApiSettingsPageContribution { label: string; settingsKey?: string; order?: number; render: () => HTMLElement | string } +interface PluginApiPanelContribution { label: string; order?: number; render: () => HTMLElement | string } +interface PluginApiChannelSectionContribution { label: string; type?: 'audio' | 'video' | 'custom'; order?: number } +interface PluginApiActionContribution { label: string; icon?: string; run: (context: PluginApiActionContext) => Promise | void } +interface PluginApiEmbedRendererContribution { embedType: string; render: (payload: unknown) => HTMLElement | string } +interface PluginApiDomMountRequest { target: Element | string; element: HTMLElement; position?: InsertPosition } + +interface TojuClientPluginApi { + readonly context: { getCurrent: () => PluginApiActionContext }; + readonly logger: { + debug: (message: string, data?: unknown) => void; + info: (message: string, data?: unknown) => void; + warn: (message: string, data?: unknown) => void; + error: (message: string, data?: unknown) => void; + }; + readonly profile: { + getCurrent: () => User | null; + update: (profile: PluginApiProfileUpdate) => void; + updateAvatar: (avatar: PluginApiAvatarUpdate) => void; + }; + readonly users: { + getCurrent: () => User | null; + list: () => User[]; + readMembers: () => RoomMember[]; + setRole: (userId: string, role: UserRole) => void; + kick: (userId: string) => void; + ban: (userId: string, reason?: string) => void; + }; + readonly roles: { + list: () => RoomRole[]; + setAssignments: (assignments: RoomRoleAssignment[]) => void; + }; + readonly server: { + getCurrent: () => Room | null; + registerPluginUser: (request: PluginApiPluginUserRequest) => string; + updatePermissions: (permissions: Partial) => void; + updateSettings: (settings: PluginApiServerSettingsUpdate) => void; + }; + readonly channels: { + list: () => Channel[]; + select: (channelId: string) => void; + addAudioChannel: (request: PluginApiChannelRequest) => void; + addVideoChannel: (request: PluginApiChannelRequest) => void; + rename: (channelId: string, name: string) => void; + remove: (channelId: string) => void; + }; + readonly messages: { + readCurrent: () => Message[]; + send: (content: string, channelId?: string) => Message; + sendAsPluginUser: (request: PluginApiMessageAsPluginUserRequest) => void; + setTyping: (isTyping: boolean, channelId?: string) => void; + subscribeTyping: (handler: (event: PluginApiTypingEvent) => void) => TojuPluginDisposable; + edit: (messageId: string, content: string) => void; + delete: (messageId: string) => void; + moderateDelete: (messageId: string) => void; + sync: (messages: Message[]) => void; + }; + readonly events: { + publishServer: (eventName: string, payload: unknown) => void; + subscribeServer: (subscription: PluginApiEventSubscription) => TojuPluginDisposable; + publishP2p: (eventName: string, payload: unknown) => void; + subscribeP2p: (subscription: PluginApiEventSubscription) => TojuPluginDisposable; + }; + readonly messageBus: { + publish: (request: PluginApiMessageBusPublishRequest) => PluginApiMessageBusEnvelope; + sendLatestMessages: (request?: PluginApiMessageBusLatestRequest) => PluginApiMessageBusEnvelope; + subscribe: (subscription: PluginApiMessageBusSubscription) => TojuPluginDisposable; + }; + readonly p2p: { + connectedPeers: () => string[]; + broadcastData: (eventName: string, payload: unknown) => void; + sendData: (peerId: string, eventName: string, payload: unknown) => void; + }; + readonly media: { + playAudioClip: (request: PluginApiAudioClipRequest) => Promise; + addCustomAudioStream: (request: PluginApiCustomStreamRequest) => Promise; + addCustomVideoStream: (request: PluginApiCustomStreamRequest) => Promise; + setInputVolume: (volume: number) => void; + setOutputVolume: (volume: number) => void; + }; + readonly clientData: { + read: (key: string) => Promise; + write: (key: string, value: unknown) => Promise; + remove: (key: string) => Promise; + }; + readonly serverData: { + read: (key: string) => Promise; + write: (key: string, value: unknown) => Promise; + remove: (key: string) => Promise; + }; + readonly storage: { + get: (key: string) => unknown; + set: (key: string, value: unknown) => void; + remove: (key: string) => void; + }; + readonly ui: { + registerAppPage: (id: string, contribution: PluginApiPageContribution) => TojuPluginDisposable; + registerSettingsPage: (id: string, contribution: PluginApiSettingsPageContribution) => TojuPluginDisposable; + registerSidePanel: (id: string, contribution: PluginApiPanelContribution) => TojuPluginDisposable; + registerChannelSection: (id: string, contribution: PluginApiChannelSectionContribution) => TojuPluginDisposable; + registerComposerAction: (id: string, contribution: PluginApiActionContribution) => TojuPluginDisposable; + registerProfileAction: (id: string, contribution: PluginApiActionContribution) => TojuPluginDisposable; + registerToolbarAction: (id: string, contribution: PluginApiActionContribution) => TojuPluginDisposable; + registerEmbedRenderer: (id: string, contribution: PluginApiEmbedRendererContribution) => TojuPluginDisposable; + mountElement: (id: string, request: PluginApiDomMountRequest) => TojuPluginDisposable; + }; +} +``` + +## API Details And Examples + +### Context And Logger + +Capabilities: none. + +```js +const current = api.context.getCurrent(); +api.logger.info('Current context', { + userId: current.user?.id, + serverId: current.server?.id, + textChannelId: current.textChannel?.id, + voiceChannelId: current.voiceChannel?.id +}); +``` + +`context.getCurrent()` returns local snapshots for the current user, current server, active text channel, and the user's current voice channel. `logger.debug/info/warn/error` writes to plugin logs in plugin management UI. Do not log secrets. + +### Profile + +Capabilities: `profile.read`, `profile.write`. + +```js +const currentUser = api.profile.getCurrent(); + +api.profile.update({ + displayName: 'Ludde the Builder', + description: 'Building plugins for MetoYou.' +}); + +api.profile.updateAvatar({ + avatarUrl: 'https://example.com/avatar.png', + avatarMime: 'image/png', + avatarHash: 'sha256:0e5751c026e543b2e8ab2eb06099daa1' +}); +``` + +### Users And Roles + +Capabilities: `users.read`, `users.manage`, `roles.read`, `roles.manage`. + +```js +const knownUsers = api.users.list(); +const currentMembers = api.users.readMembers(); +const roles = api.roles.list(); + +api.users.setRole('5749584c-4ae6-44c1-b901-81ed4a80be63', 'moderator'); + +api.roles.setAssignments([ + { + userId: '5749584c-4ae6-44c1-b901-81ed4a80be63', + oderId: '5749584c-4ae6-44c1-b901-81ed4a80be63', + roleIds: ['moderator'] + } +]); +``` + +Moderation examples, only behind explicit user action: + +```js +api.users.kick('5749584c-4ae6-44c1-b901-81ed4a80be63'); +api.users.ban('5749584c-4ae6-44c1-b901-81ed4a80be63', 'Repeated spam after warning.'); +``` + +### Server + +Capabilities: `server.read`, `server.manage`, `users.manage` for plugin users. + +```js +const server = api.server.getCurrent(); + +api.server.updateSettings({ + name: 'Friday Build Room', + description: 'A server for focused testing sessions.', + topic: 'Plugin QA and voice checks', + isPrivate: false, + maxUsers: 24 +}); + +api.server.updatePermissions({ + allowVoice: true, + allowScreenShare: true, + allowFileUploads: true, + slowModeInterval: 0 +}); + +const botUserId = api.server.registerPluginUser({ + id: 'example.my-plugin:announcer', + displayName: 'Plugin Announcer', + avatarUrl: 'https://example.com/plugin-announcer.png' +}); +``` + +`registerPluginUser` creates a locally visible plugin-owned user record and returns its id. Reuse that id with `messages.sendAsPluginUser`. + +### Channels + +Capabilities: `channels.read`, `channels.manage`. + +```js +const channels = api.channels.list(); +const textChannels = channels.filter((channel) => channel.type === 'text'); +const voiceChannels = channels.filter((channel) => channel.type === 'voice'); + +api.channels.select('general'); +api.channels.addAudioChannel({ id: 'standup-voice', name: 'Standup Voice', position: 200 }); +api.channels.addVideoChannel({ id: 'demo-stage', name: 'Demo Stage', position: 300 }); +api.channels.rename('standup-voice', 'Daily Standup'); +api.channels.remove('demo-stage'); +``` + +`addAudioChannel` creates a voice channel in room state. `addVideoChannel` registers a plugin video channel section contribution, not a normal `Channel` model entry. + +### Messages And Typing + +Capabilities: `messages.read`, `messages.send`, `messages.editOwn`, `messages.deleteOwn`, `messages.moderate`, `messages.sync`. + +```js +const visibleMessages = api.messages.readCurrent(); + +const sent = api.messages.send( + 'Build completed successfully. Docs are ready for review.', + 'general' +); + +api.messages.edit(sent.id, 'Build completed successfully. Docs and plugin examples are ready.'); +api.messages.delete(sent.id); +``` + +Plugin-user message: + +```js +const botUserId = api.server.registerPluginUser({ + id: 'example.my-plugin:status-bot', + displayName: 'Status Bot' +}); + +api.messages.sendAsPluginUser({ + pluginUserId: botUserId, + channelId: 'general', + content: 'Voice check starts in 5 minutes.' +}); +``` + +Typing state: + +```js +api.messages.setTyping(true, 'general'); + +setTimeout(() => { + api.messages.setTyping(false, 'general'); +}, 3000); + +const typingSubscription = api.messages.subscribeTyping((event) => { + api.logger.info('Typing event', { + channelId: event.channelId, + displayName: event.displayName, + isTyping: event.isTyping, + serverId: event.serverId, + userId: event.userId + }); +}); + +context.subscriptions.push(typingSubscription); +``` + +`messages.send` creates a message, persists it locally, dispatches it into the local message store, and broadcasts a `chat-message` peer event. `edit` and `delete` update local persistence and broadcast peer edit/delete events. `moderateDelete` should require explicit confirmation. `sync` injects an array of `Message` objects into state and should be used only by plugins that intentionally bridge or restore messages. + +### Events + +Capabilities: `events.server.publish`, `events.server.subscribe`, `events.p2p.publish`, `events.p2p.subscribe`. + +Manifest declaration is required: + +```json +{ + "events": [ + { + "eventName": "example.my-plugin.poll-vote", + "direction": "serverRelay", + "scope": "server", + "maxPayloadBytes": 4096, + "schema": "{\"type\":\"object\",\"required\":[\"pollId\",\"optionId\"]}" + } + ] +} +``` + +```js +api.events.publishServer('example.my-plugin.poll-vote', { + pollId: 'poll-2026-04-29-standup', + optionId: 'ship-it', + votedAt: Date.now() +}); + +const serverEventSubscription = api.events.subscribeServer({ + eventName: 'example.my-plugin.poll-vote', + handler: (event) => { + api.logger.info('Poll vote received', event.payload); + } +}); + +context.subscriptions.push(serverEventSubscription); +``` + +Important runtime detail: `subscribeServer` listens to signaling messages and calls the handler. `subscribeP2p` currently records/logs the subscription; for rich peer-to-peer plugin synchronization, prefer `api.messageBus`. + +### Message Bus + +Capabilities: `events.p2p.publish`, `events.p2p.subscribe`; also `messages.read` when including or replaying latest messages. + +The message bus sends `plugin-message-bus` data-channel events. It does not create normal chat messages. + +```js +const subscription = api.messageBus.subscribe({ + topic: 'example.my-plugin.checklist-state', + channelId: 'general', + replayLatest: true, + latestMessageLimit: 25, + handler: (event) => { + api.logger.info('Checklist bus event', { + topic: event.topic, + latestMessageCount: event.messages?.length ?? 0, + sourceUserId: event.sourceUserId + }); + } +}); + +context.subscriptions.push(subscription); + +const envelope = api.messageBus.publish({ + topic: 'example.my-plugin.checklist-state', + channelId: 'general', + includeSelf: true, + includeLatestMessages: true, + limit: 20, + payload: { + items: [ + { id: 'docs', label: 'Review docs', done: true }, + { id: 'voice', label: 'Test voice join', done: false } + ], + updatedAt: Date.now() + } +}); + +api.logger.debug('Published bus envelope', envelope); +``` + +Latest message snapshots default to `50` messages and are clamped to `1..250`. + +### P2P + +Capabilities: `p2p.data`. + +```js +const peerIds = api.p2p.connectedPeers(); + +api.p2p.broadcastData('example.my-plugin.presence-ping', { + status: 'reviewing-docs', + sentAt: Date.now() +}); + +if (peerIds.length > 0) { + api.p2p.sendData(peerIds[0], 'example.my-plugin.private-nudge', { + message: 'Can you check the voice channel?' + }); +} +``` + +`connectedPeers()` returns ids from the voice/WebRTC connection facade. + +### Media + +Capabilities: `media.playAudio`, `media.addAudioStream`, `media.addVideoStream`, `audio.volume`. + +```js +await api.media.playAudioClip({ + url: 'https://example.com/sounds/ding.mp3', + volume: 0.35 +}); + +api.media.setInputVolume(0.8); +api.media.setOutputVolume(0.6); +``` + +Create and contribute a browser `MediaStream`: + +```js +const audioContext = new AudioContext(); +const oscillator = audioContext.createOscillator(); +const destination = audioContext.createMediaStreamDestination(); + +oscillator.frequency.value = 440; +oscillator.connect(destination); +oscillator.start(); + +await api.media.addCustomAudioStream({ + label: 'Generated tone', + stream: destination.stream +}); +``` + +`addCustomAudioStream` currently sets the local voice stream through the voice facade. `addCustomVideoStream` registers/logs a video contribution; do not assume custom video rendering is complete unless the target app version confirms it. Audio clip volume is clamped to `0..1`. + +### Storage + +Capabilities: `storage.local`, `storage.serverData.read`, `storage.serverData.write`. + +Use async APIs for new plugins: + +```js +await api.clientData.write('preferences', { + compactMode: true, + favoriteChannelIds: ['general', 'standup-voice'], + updatedAt: Date.now() +}); + +const preferences = await api.clientData.read('preferences'); + +await api.serverData.write('server-checklist', { + items: [ + { id: 'setup', label: 'Create server channels', done: true }, + { id: 'invite', label: 'Invite test user', done: false } + ], + updatedAt: Date.now() +}); + +const checklist = await api.serverData.read('server-checklist'); +await api.serverData.remove('server-checklist'); +``` + +Legacy synchronous local storage: + +```js +api.storage.set('lastPanelTab', 'overview'); +const lastPanelTab = api.storage.get('lastPanelTab'); +api.storage.remove('lastPanelTab'); +``` + +Desktop uses Electron's local database when available, with renderer localStorage fallback. Browser-only clients use localStorage. `serverData` throws if no server is active. + +### UI + +Capabilities: + +| Method | Required capability | +| --- | --- | +| `registerAppPage` | `ui.pages` | +| `registerSettingsPage` | `ui.settings` | +| `registerSidePanel` | `ui.sidePanel` | +| `registerChannelSection` | `ui.channelsSection` | +| `registerComposerAction` | `ui.pages` | +| `registerProfileAction` | `ui.pages` | +| `registerToolbarAction` | `ui.pages` | +| `registerEmbedRenderer` | `ui.embeds` | +| `mountElement` | `ui.dom` | + +Register side panel: + +```js +context.subscriptions.push(api.ui.registerSidePanel('summary', { + label: 'Plugin Summary', + order: 10, + render: () => { + const root = document.createElement('aside'); + const heading = document.createElement('h2'); + const text = document.createElement('p'); + + heading.textContent = 'Plugin Summary'; + text.textContent = 'No active tasks.'; + root.append(heading, text); + return root; + } +})); +``` + +Use `registerSidePanel` for content that belongs in the server sidebar plugin area. Do not query `[data-testid="plugin-room-side-panel"]` and pass it to `mountElement`; that route-specific host may not exist while the plugin activates. + +Register app page: + +```js +context.subscriptions.push(api.ui.registerAppPage('dashboard', { + label: 'Build Dashboard', + path: '/plugins/example.build-dashboard/dashboard', + render: () => { + const root = document.createElement('section'); + const title = document.createElement('h1'); + const button = document.createElement('button'); + const output = document.createElement('p'); + + title.textContent = 'Build Dashboard'; + button.type = 'button'; + button.textContent = 'Send status'; + output.textContent = 'Idle.'; + + button.addEventListener('click', () => { + const message = api.messages.send('Build dashboard status: ready.'); + output.textContent = `Sent message ${message.id}`; + }); + + root.append(title, button, output); + return root; + } +})); +``` + +Register actions: + +```js +context.subscriptions.push(api.ui.registerComposerAction('insert-template', { + label: 'Insert Template', + icon: 'file-text', + run: (actionContext) => { + api.messages.send( + 'Template: Please review the latest build notes.', + actionContext.textChannel?.id + ); + } +})); + +context.subscriptions.push(api.ui.registerToolbarAction('post-standup', { + label: 'Post Standup', + icon: 'megaphone', + run: () => { + api.messages.send('Standup starts now. Join the voice channel when ready.'); + } +})); +``` + +Mount DOM directly: + +Use direct DOM mounting only for targets that exist now, or after your plugin has explicitly checked the target. `body` is safe during activation. Route-specific selectors are not safe during activation. + +```js +const banner = document.createElement('div'); +banner.textContent = 'Plugin banner mounted in chat messages.'; + +const target = document.querySelector('app-chat-messages'); + +if (target) { + context.subscriptions.push(api.ui.mountElement('chat-banner', { + target, + element: banner, + position: 'afterbegin' + })); +} +``` + +Global overlay example: + +```js +const badge = document.createElement('div'); +badge.textContent = 'Plugin active'; + +context.subscriptions.push(api.ui.mountElement('global-badge', { + target: 'body', + element: badge, + position: 'beforeend' +})); +``` + +`mountElement` tags the element with plugin ownership metadata, replaces duplicate mounts for the same plugin/id, and removes it on disposal/unload. + +## Capability Cheat Sheet + +| API call group | Capabilities | +| --- | --- | +| `profile.getCurrent` | `profile.read` | +| `profile.update`, `profile.updateAvatar` | `profile.write` | +| `users.getCurrent`, `users.list`, `users.readMembers` | `users.read` | +| `users.kick`, `users.ban`, `server.registerPluginUser` | `users.manage` | +| `roles.list` | `roles.read` | +| `users.setRole`, `roles.setAssignments` | `roles.manage` | +| `server.getCurrent` | `server.read` | +| `server.updatePermissions`, `server.updateSettings` | `server.manage` | +| `channels.list`, `channels.select` | `channels.read` | +| `channels.addAudioChannel`, `channels.addVideoChannel`, `channels.rename`, `channels.remove` | `channels.manage` | +| `messages.readCurrent`, `messages.subscribeTyping` | `messages.read` | +| `messages.send`, `messages.sendAsPluginUser`, `messages.setTyping` | `messages.send` | +| `messages.edit` | `messages.editOwn` | +| `messages.delete` | `messages.deleteOwn` | +| `messages.moderateDelete` | `messages.moderate` | +| `messages.sync` | `messages.sync` | +| `events.publishServer` | `events.server.publish` | +| `events.subscribeServer` | `events.server.subscribe` | +| `events.publishP2p` | `events.p2p.publish` | +| `events.subscribeP2p` | `events.p2p.subscribe` | +| `messageBus.publish` | `events.p2p.publish`, plus `messages.read` when `includeLatestMessages` is true | +| `messageBus.sendLatestMessages` | `events.p2p.publish`, `messages.read` | +| `messageBus.subscribe` | `events.p2p.subscribe`, plus `messages.read` when `replayLatest` is true | +| `p2p.*` | `p2p.data` | +| `media.playAudioClip` | `media.playAudio` | +| `media.addCustomAudioStream` | `media.addAudioStream` | +| `media.addCustomVideoStream` | `media.addVideoStream` | +| `media.setInputVolume`, `media.setOutputVolume` | `audio.volume` | +| `clientData.*`, `storage.*` | `storage.local` | +| `serverData.read` | `storage.serverData.read` | +| `serverData.write`, `serverData.remove` | `storage.serverData.write` | +| `ui.registerAppPage`, composer/profile/toolbar actions | `ui.pages` | +| `ui.registerSettingsPage` | `ui.settings` | +| `ui.registerSidePanel` | `ui.sidePanel` | +| `ui.registerChannelSection` | `ui.channelsSection` | +| `ui.registerEmbedRenderer` | `ui.embeds` | +| `ui.mountElement` | `ui.dom` | + +## Complete Example Plugin + +`toju-plugin.json`: + +```json +{ + "schemaVersion": 1, + "id": "example.voice-notes", + "title": "Voice Notes", + "description": "Adds a server panel for posting voice-session notes and syncing draft state with peers.", + "version": "1.0.0", + "kind": "client", + "scope": "server", + "apiVersion": "1.0.0", + "compatibility": { + "minimumTojuVersion": "1.0.0" + }, + "entrypoint": "./main.js", + "capabilities": [ + "server.read", + "channels.read", + "messages.read", + "messages.send", + "events.p2p.publish", + "events.p2p.subscribe", + "storage.serverData.read", + "storage.serverData.write", + "ui.sidePanel", + "ui.pages" + ] +} +``` + +`main.js`: + +```js +const DRAFT_KEY = 'voice-notes-draft'; +const BUS_TOPIC = 'example.voice-notes.draft'; + +export function activate(context) { + const { api } = context; + + api.logger.info('Voice Notes activated'); + + context.subscriptions.push(api.messageBus.subscribe({ + topic: BUS_TOPIC, + replayLatest: false, + handler: (event) => { + api.logger.debug('Received voice notes draft update', event.payload); + } + })); + + context.subscriptions.push(api.ui.registerSidePanel('voice-notes-panel', { + label: 'Voice Notes', + order: 20, + render: () => renderPanel(context) + })); + + context.subscriptions.push(api.ui.registerAppPage('voice-notes', { + label: 'Voice Notes', + path: '/plugins/example.voice-notes/voice-notes', + render: () => renderPanel(context) + })); +} + +function renderPanel(context) { + const { api } = context; + const root = document.createElement('section'); + const heading = document.createElement('h2'); + const meta = document.createElement('p'); + const textarea = document.createElement('textarea'); + const save = document.createElement('button'); + const post = document.createElement('button'); + const status = document.createElement('p'); + + const current = api.context.getCurrent(); + heading.textContent = 'Voice Notes'; + meta.textContent = current.voiceChannel + ? `Connected to ${current.voiceChannel.name}` + : 'Not connected to a voice channel.'; + textarea.rows = 6; + textarea.placeholder = 'Write notes from the current voice session.'; + save.type = 'button'; + save.textContent = 'Save Draft'; + post.type = 'button'; + post.textContent = 'Post Notes'; + status.textContent = 'Loading draft...'; + + void api.serverData.read(DRAFT_KEY).then((value) => { + if (value && typeof value === 'object' && typeof value.text === 'string') { + textarea.value = value.text; + } + + status.textContent = 'Draft loaded.'; + }).catch((error) => { + api.logger.warn('Could not load voice notes draft', error); + status.textContent = 'Could not load draft.'; + }); + + save.addEventListener('click', async () => { + const draft = { + text: textarea.value, + serverId: api.server.getCurrent()?.id ?? null, + channelId: api.context.getCurrent().textChannel?.id ?? null, + updatedAt: Date.now() + }; + + await api.serverData.write(DRAFT_KEY, draft); + api.messageBus.publish({ topic: BUS_TOPIC, includeSelf: false, payload: draft }); + status.textContent = 'Draft saved.'; + }); + + post.addEventListener('click', () => { + const text = textarea.value.trim(); + + if (!text) { + status.textContent = 'Write a note before posting.'; + return; + } + + api.messages.send(`Voice notes:\n\n${text}`, api.context.getCurrent().textChannel?.id); + status.textContent = 'Posted to the current text channel.'; + }); + + root.append(heading, meta, textarea, save, post, status); + return root; +} + +export function deactivate(context) { + context.api.logger.info('Voice Notes deactivated'); +} +``` + +## Final Checklist For Generated Plugins + +- Manifest is valid JSON with no comments. +- Manifest id is lowercase dotted or dashed. +- Manifest capabilities exactly match API calls used in `main.js`. +- Every `api.events.*` event name is declared in `events`. +- `main.js` exports `activate`; `ready` and `deactivate` are optional. +- Every subscription/disposable is pushed into `context.subscriptions`. +- Plugin uses browser DOM APIs and browser globals only. +- No destructive API runs automatically during activation. +- UI uses real `button`, `label`, `input`, `textarea`, headings, and status text. +- Async storage and media calls are awaited or handled with `.then/.catch`. +- README explains behavior, capabilities, and installation. + +## More Reference + +- User-facing behavior: User Guide pages. +- DOM structure: Developer Guide -> App Pages and DOM Structure. +- Local REST API: Developer Guide -> Local REST API. +- Plugin manifest: Plugin Development -> Manifest Model. +- Capabilities: Plugin Development -> Capabilities. +- Focused plugin API examples: Plugin Development -> API Reference and its API subpages. \ No newline at end of file diff --git a/docs-site/docs/developer/rest-api.md b/docs-site/docs/developer/rest-api.md new file mode 100644 index 0000000..a056ed0 --- /dev/null +++ b/docs-site/docs/developer/rest-api.md @@ -0,0 +1,300 @@ +--- +sidebar_position: 4 +--- + +# Local REST API + +The MetoYou desktop app exposes an optional local HTTP API for scripts and tools. It is implemented in Electron and reads local desktop data. + +## Enable the API + +1. Open Settings. +2. Open Local API settings. +3. Enable the local server. +4. Choose a port. The default is `17878`. +5. Add trusted signaling server URLs for authentication. +6. Enable Scalar docs if you want `/docs`. +7. Enable Docusaurus docs if you want `/docusaurus`. + +By default the server binds to `127.0.0.1`. Only enable LAN exposure when you understand the risk. + +## Authentication + +Protected routes require a bearer token. Get one by posting username, password, and an allowed signaling server URL. + +```bash +curl -s http://127.0.0.1:17878/api/auth/login \ + -H 'Content-Type: application/json' \ + -d '{ + "username": "alice", + "password": "correct horse battery staple", + "serverUrl": "https://tojusignal.example.com" + }' +``` + +Example response: + +```json +{ + "token": "local_4cddf95c5b8c4b6f9e0c", + "expiresAt": 1777477200000, + "user": { + "id": "user-alice-01", + "username": "alice", + "displayName": "Alice" + } +} +``` + +Use the token: + +```bash +curl -s http://127.0.0.1:17878/api/profile \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +Logout revokes the current token: + +```bash +curl -i -X POST http://127.0.0.1:17878/api/auth/logout \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +## OpenAPI and Scalar + +| Route | Auth | Purpose | +| --- | --- | --- | +| `GET /api/openapi.json` | No | OpenAPI 3.1 document. | +| `GET /docs` | No | Scalar API reference when enabled. | + +## Public Routes + +### GET /api/health + +Checks whether the local API server is running. + +```bash +curl -s http://127.0.0.1:17878/api/health +``` + +Example response: + +```json +{ + "status": "ok", + "version": "1.0.0", + "timestamp": 1777473600000, + "exposeOnLan": false +} +``` + +### GET /api/openapi.json + +Returns the machine-readable API document. + +```bash +curl -s http://127.0.0.1:17878/api/openapi.json +``` + +### POST /api/auth/login + +Issues a local bearer token after credentials are validated by an allowed signaling server. + +Request body: + +```json +{ + "username": "alice", + "password": "correct horse battery staple", + "serverUrl": "https://tojusignal.example.com" +} +``` + +Common errors: + +| Status | Error code | Meaning | +| --- | --- | --- | +| 400 | `INVALID_REQUEST` | Missing username, password, or server URL. | +| 403 | `NO_ALLOWED_SERVERS` | No allowed signaling servers are configured. | +| 403 | `SERVER_NOT_ALLOWED` | The server URL is not in the allowed list. | +| 401 | `INVALID_CREDENTIALS` | Signaling server rejected the login. | +| 502 | `UPSTREAM_UNREACHABLE` | The signaling server could not be reached. | + +## Protected Routes + +All routes below require: + +```http +Authorization: Bearer local_4cddf95c5b8c4b6f9e0c +``` + +### GET /api/profile + +Reads the current local user profile. + +```bash +curl -s http://127.0.0.1:17878/api/profile \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET /api/rooms + +Lists rooms known to this device. + +```bash +curl -s http://127.0.0.1:17878/api/rooms \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/rooms/{roomId}` + +Reads one room by id. + +```bash +curl -s http://127.0.0.1:17878/api/rooms/room-7ebdde75 \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/rooms/{roomId}/users` + +Lists users known for a room. + +```bash +curl -s http://127.0.0.1:17878/api/rooms/room-7ebdde75/users \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/rooms/{roomId}/messages` + +Lists local messages for a room. `limit` defaults to `100` and is clamped from `1` to `500`. `offset` defaults to `0`. + +```bash +curl -s 'http://127.0.0.1:17878/api/rooms/room-7ebdde75/messages?limit=50&offset=0' \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/rooms/{roomId}/messages/since` + +Lists local messages after a required timestamp. + +```bash +curl -s 'http://127.0.0.1:17878/api/rooms/room-7ebdde75/messages/since?sinceTimestamp=1777470000000' \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/rooms/{roomId}/bans` + +Lists active bans for a room. + +```bash +curl -s http://127.0.0.1:17878/api/rooms/room-7ebdde75/bans \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/rooms/{roomId}/bans/{userId}` + +Checks whether a user is banned in a room. + +```bash +curl -s http://127.0.0.1:17878/api/rooms/room-7ebdde75/bans/user-muse-01 \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +Example response: + +```json +{ "isBanned": false } +``` + +### GET `/api/messages/{messageId}` + +Reads one local message by id. + +```bash +curl -s http://127.0.0.1:17878/api/messages/msg-20260429-001 \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/messages/{messageId}/reactions` + +Lists reactions for a message. + +```bash +curl -s http://127.0.0.1:17878/api/messages/msg-20260429-001/reactions \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/messages/{messageId}/attachments` + +Lists attachments for a message. + +```bash +curl -s http://127.0.0.1:17878/api/messages/msg-20260429-001/attachments \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET `/api/users/{userId}` + +Reads one user by id. + +```bash +curl -s http://127.0.0.1:17878/api/users/user-muse-01 \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET /api/attachments + +Lists all attachments stored on this device. + +```bash +curl -s http://127.0.0.1:17878/api/attachments \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +### GET /api/plugin-data + +Reads a plugin data value from the local desktop database. `scope` must be `local` or `server`. Provide `serverId` when reading server-scoped data. + +```bash +curl -s 'http://127.0.0.1:17878/api/plugin-data?pluginId=example.soundboard&key=favorites&scope=server&serverId=room-7ebdde75' \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +Example response: + +```json +{ + "value": [ + { "label": "Chime", "url": "https://cdn.example.com/chime.wav" } + ] +} +``` + +### GET `/api/meta/{key}` + +Reads a desktop metadata value by key. + +```bash +curl -s http://127.0.0.1:17878/api/meta/metoyou_currentUserId \ + -H 'Authorization: Bearer local_4cddf95c5b8c4b6f9e0c' +``` + +Example response: + +```json +{ + "key": "metoyou_currentUserId", + "value": "user-alice-01" +} +``` + +## Data Model Notes + +Rooms, users, messages, reactions, attachments, and bans are returned from local desktop persistence. Many schemas allow additional properties because the local database can carry richer app state than the REST docs need to guarantee. + +## Security Notes + +- Keep the API bound to `127.0.0.1` unless LAN access is required. +- Only add signaling servers you trust to the allowed list. +- Bearer tokens are local to the running desktop app. +- Stop the local API server to clear issued tokens. \ No newline at end of file diff --git a/docs-site/docs/intro.md b/docs-site/docs/intro.md new file mode 100644 index 0000000..de58d85 --- /dev/null +++ b/docs-site/docs/intro.md @@ -0,0 +1,48 @@ +--- +slug: / +sidebar_position: 1 +--- + +# MetoYou Documentation + +MetoYou is a desktop-first chat app with text channels, voice channels, direct messages, plugins, local desktop storage, a local REST API, and a Docusaurus documentation site bundled into the app. + +This site is split into three paths: + +- **User Guide** explains the app in non-technical terms: servers, text channels, voice channels, screen sharing, direct messages, plugins, and desktop settings. +- **Developer Guide** explains how to run the repo, how the app is structured, how Docusaurus is served, the app DOM/page structure, and the local REST API. +- **Plugin Development** explains how to build plugins, declare capabilities, distribute bundles, and call every exposed plugin API with concrete examples. + +The Electron app can host this documentation locally. The docs endpoint is not a separate web server process: it is served from the same opt-in local HTTP server used for the Local API, and it only serves static files generated by Docusaurus. + +## What Is Included + +| Area | What it covers | +| --- | --- | +| Product client | Login, server discovery, channels, messages, voice, direct messages, themes, and plugin UI. | +| Desktop shell | Window controls, notifications, tray behavior, app data import/export, updates, local plugins, and hosted documentation. | +| Local HTTP API | A loopback-first API for local scripts and tools, with OpenAPI and Scalar reference docs. | +| Plugin runtime | Browser-safe client plugins with explicit capabilities, lifecycle hooks, UI contributions, data storage, message bus, and server plugin requirements. | + +## Runtime Boundaries + +MetoYou keeps responsibilities split by package: + +- `toju-app/` is the Angular product client and plugin runtime. +- `electron/` is the main process, preload bridge, IPC, local persistence, and local HTTP host. +- `server/` is the signaling and server-directory service. +- `e2e/` contains Playwright coverage for browser and WebRTC workflows. +- `docs-site/` is this Docusaurus site. + +The desktop documentation endpoint serves the static `docs-site/build` output. It does not run the Docusaurus development server inside Electron. + +## Fast Links + +- Start using the app: [First Steps](./user-guide/first-steps.md) +- Join voice: [Voice Channels and Calls](./user-guide/voice-channels.md) +- Install plugins: [Plugins for Users](./user-guide/plugins.md) +- Run the repo: [Contributing](./developer/contributing.md) +- Understand pages and DOM: [App Pages and DOM Structure](./developer/dom-structure.md) +- Use the REST API: [Local REST API](./developer/rest-api.md) +- Build a plugin: [Create a Plugin](./plugin-development/create-a-plugin.md) +- Give an LLM plugin context: [LLM Plugin Builder Guide](./developer/llm-plugin-builder-guide.md) diff --git a/docs-site/docs/plugin-development/api-reference.md b/docs-site/docs/plugin-development/api-reference.md new file mode 100644 index 0000000..772ac48 --- /dev/null +++ b/docs-site/docs/plugin-development/api-reference.md @@ -0,0 +1,329 @@ +--- +sidebar_position: 4 +--- + +# Plugin API Reference + +`TojuClientPluginApi` is the object passed to a plugin activation context. The runtime freezes the API object before passing it to plugin code. + +This page is the compact map. Use the focused API pages for concrete copy-paste examples with literal input data. + +## Focused API Pages + +- [Context and Logging](./api/context-and-logging.md) +- [Profile API](./api/profile.md) +- [Users and Roles API](./api/users-and-roles.md) +- [Server API](./api/server.md) +- [Channels API](./api/channels.md) +- [Messages and Typing API](./api/messages-and-typing.md) +- [Events API](./api/events.md) +- [Message Bus API](./api/message-bus.md) +- [P2P and Media API](./api/p2p-and-media.md) +- [Storage API](./api/storage.md) +- [UI API](./api/ui.md) + +## Activation Types + +```ts +interface TojuPluginDisposable { + dispose: () => void; +} + +interface TojuPluginActivationContext { + api: TojuClientPluginApi; + manifest: TojuPluginManifest; + pluginId: string; + subscriptions: TojuPluginDisposable[]; +} + +interface TojuClientPluginModule { + activate?: (context: TojuPluginActivationContext) => Promise | void; + deactivate?: (context: TojuPluginActivationContext) => Promise | void; + onPluginDataChanged?: (context: TojuPluginActivationContext, event: unknown) => Promise | void; + onServerRequirementsChanged?: (context: TojuPluginActivationContext, snapshot: PluginRequirementsSnapshot) => Promise | void; + ready?: (context: TojuPluginActivationContext) => Promise | void; +} +``` + +## Profiles + +```ts +interface PluginApiProfileUpdate { + description?: string; + displayName: string; +} + +interface PluginApiAvatarUpdate { + avatarHash: string; + avatarMime: string; + avatarUrl: string; +} +``` + +| Method | Capability | Description | +| --- | --- | --- | +| `profile.getCurrent()` | `profile.read` | Returns the current `User` or `null`. | +| `profile.update(profile)` | `profile.write` | Updates display name and optional description. | +| `profile.updateAvatar(avatar)` | `profile.write` | Updates avatar URL, MIME type, and hash metadata. | + +## Users and Roles + +| Method | Capability | Description | +| --- | --- | --- | +| `users.getCurrent()` | `users.read` | Returns current `User` or `null`. | +| `users.list()` | `users.read` | Returns known users. | +| `users.readMembers()` | `users.read` | Returns active room members. | +| `users.setRole(userId, role)` | `roles.manage` | Updates a user's role. | +| `users.kick(userId)` | `users.manage` | Kicks a user. | +| `users.ban(userId, reason?)` | `users.manage` | Bans a user with optional reason. | +| `roles.list()` | `roles.read` | Returns room roles. | +| `roles.setAssignments(assignments)` | `roles.manage` | Replaces role assignments. | + +## Server + +```ts +interface PluginApiServerSettingsUpdate { + description?: string; + isPrivate?: boolean; + maxUsers?: number; + name?: string; + password?: string; + topic?: string; +} + +interface PluginApiPluginUserRequest { + avatarUrl?: string; + displayName: string; + id?: string; +} +``` + +| Method | Capability | Description | +| --- | --- | --- | +| `server.getCurrent()` | `server.read` | Returns the current `Room` or `null`. | +| `server.registerPluginUser(request)` | `users.manage` | Adds a plugin-owned user and returns its id. | +| `server.updatePermissions(permissions)` | `server.manage` | Updates partial room permissions. | +| `server.updateSettings(settings)` | `server.manage` | Updates room settings. | + +## Channels + +```ts +interface PluginApiChannelRequest { + id?: string; + name: string; + position?: number; +} +``` + +| Method | Capability | Description | +| --- | --- | --- | +| `channels.list()` | `channels.read` | Returns current room channels. | +| `channels.select(channelId)` | `channels.read` | Selects a channel. | +| `channels.addAudioChannel(request)` | `channels.manage` | Adds a voice channel. | +| `channels.addVideoChannel(request)` | `channels.manage` | Registers a video channel section. | +| `channels.rename(channelId, name)` | `channels.manage` | Renames a channel. | +| `channels.remove(channelId)` | `channels.manage` | Removes a channel. | + +## Messages + +```ts +interface PluginApiMessageAsPluginUserRequest { + channelId?: string; + content: string; + pluginUserId: string; +} +``` + +| Method | Capability | Description | +| --- | --- | --- | +| `messages.readCurrent()` | `messages.read` | Returns current visible messages. | +| `messages.send(content, channelId?)` | `messages.send` | Sends a message and returns the created `Message`. | +| `messages.sendAsPluginUser(request)` | `messages.send` | Emits a message from a registered plugin user. | +| `messages.setTyping(isTyping, channelId?)` | `messages.send` | Broadcasts current typing state for a channel. | +| `messages.subscribeTyping(handler)` | `messages.read` | Subscribes to peer typing state. | +| `messages.edit(messageId, content)` | `messages.editOwn` | Edits a plugin message. | +| `messages.delete(messageId)` | `messages.deleteOwn` | Deletes a plugin message. | +| `messages.moderateDelete(messageId)` | `messages.moderate` | Performs a moderation delete. | +| `messages.sync(messages)` | `messages.sync` | Syncs an array of messages into state. | + +## Events + +```ts +interface PluginApiEventSubscription { + eventName: string; + handler: (event: PluginEventEnvelope) => void; +} + +interface PluginEventEnvelope { + emittedAt?: number; + eventId?: string; + eventName: string; + payload: TPayload; + pluginId: string; + serverId: string; + sourcePluginUserId?: string; + sourceUserId?: string; + type: 'plugin_event'; +} +``` + +| Method | Capability | Description | +| --- | --- | --- | +| `events.publishServer(eventName, payload)` | `events.server.publish` | Sends a declared plugin event through the signaling server. | +| `events.subscribeServer(subscription)` | `events.server.subscribe` | Subscribes to a declared server plugin event. | +| `events.publishP2p(eventName, payload)` | `events.p2p.publish` | Sends a declared plugin event over peer paths. | +| `events.subscribeP2p(subscription)` | `events.p2p.subscribe` | Registers a P2P event subscription. | + +## Message Bus + +```ts +interface PluginApiMessageBusEnvelope { + channelId?: string; + eventId: string; + messages?: Message[]; + payload?: unknown; + pluginId: string; + roomId: string; + sentAt: number; + sourcePeerId?: string; + sourceUserId?: string; + topic: string; +} + +interface PluginApiMessageBusLatestRequest { + channelId?: string; + includeDeleted?: boolean; + limit?: number; + sinceTimestamp?: number; + targetPeerId?: string; + topic?: string; +} + +interface PluginApiMessageBusPublishRequest extends PluginApiMessageBusLatestRequest { + includeLatestMessages?: boolean; + includeSelf?: boolean; + payload?: unknown; + topic: string; +} + +interface PluginApiMessageBusSubscription { + channelId?: string; + handler: (event: PluginApiMessageBusEnvelope) => void; + latestMessageLimit?: number; + replayLatest?: boolean; + topic?: string; +} +``` + +| Method | Capability | Description | +| --- | --- | --- | +| `messageBus.publish(request)` | `events.p2p.publish`, optionally `messages.read` | Publishes a plugin-bus event, optionally including latest messages. | +| `messageBus.sendLatestMessages(request?)` | `events.p2p.publish` and `messages.read` | Sends a latest-message snapshot. | +| `messageBus.subscribe(subscription)` | `events.p2p.subscribe`, optionally `messages.read` | Subscribes to plugin-bus events, optionally replaying latest messages. | + +## P2P and Media + +```ts +interface PluginApiAudioClipRequest { + volume?: number; + url: string; +} + +interface PluginApiCustomStreamRequest { + label?: string; + stream: MediaStream; +} +``` + +| Method | Capability | Description | +| --- | --- | --- | +| `p2p.connectedPeers()` | `p2p.data` | Returns connected peer ids. | +| `p2p.broadcastData(eventName, payload)` | `p2p.data` | Broadcasts plugin data. | +| `p2p.sendData(peerId, eventName, payload)` | `p2p.data` | Sends plugin data targeted to a peer. | +| `media.playAudioClip(request)` | `media.playAudio` | Plays an audio URL at optional volume. | +| `media.addCustomAudioStream(request)` | `media.addAudioStream` | Contributes an audio `MediaStream`. | +| `media.addCustomVideoStream(request)` | `media.addVideoStream` | Registers a video `MediaStream` contribution. | +| `media.setInputVolume(volume)` | `audio.volume` | Sets local input volume. | +| `media.setOutputVolume(volume)` | `audio.volume` | Sets local output volume. | + +## Storage + +| Method | Capability | Description | +| --- | --- | --- | +| `clientData.read(key)` | `storage.local` | Reads async plugin-local data. | +| `clientData.write(key, value)` | `storage.local` | Writes async plugin-local data. | +| `clientData.remove(key)` | `storage.local` | Removes async plugin-local data. | +| `serverData.read(key)` | `storage.serverData.read` | Reads local per-user/per-server data. | +| `serverData.write(key, value)` | `storage.serverData.write` | Writes local per-user/per-server data. | +| `serverData.remove(key)` | `storage.serverData.write` | Removes local per-user/per-server data. | +| `storage.get(key)` | `storage.local` | Legacy synchronous local read. | +| `storage.set(key, value)` | `storage.local` | Legacy synchronous local write. | +| `storage.remove(key)` | `storage.local` | Legacy synchronous local remove. | + +## UI Contributions + +```ts +interface PluginApiActionContribution { + icon?: string; + label: string; + run: () => Promise | void; +} + +interface PluginApiPageContribution { + label: string; + path: string; + render: () => HTMLElement | string; +} + +interface PluginApiPanelContribution { + label: string; + order?: number; + render: () => HTMLElement | string; +} + +interface PluginApiSettingsPageContribution { + label: string; + order?: number; + render: () => HTMLElement | string; + settingsKey?: string; +} + +interface PluginApiChannelSectionContribution { + label: string; + order?: number; + type?: 'audio' | 'custom' | 'video'; +} + +interface PluginApiEmbedRendererContribution { + embedType: string; + render: (payload: unknown) => HTMLElement | string; +} + +interface PluginApiDomMountRequest { + element: HTMLElement; + position?: InsertPosition; + target: Element | string; +} +``` + +| Method | Capability | Description | +| --- | --- | --- | +| `ui.registerAppPage(id, contribution)` | `ui.pages` | Adds a plugin app page. | +| `ui.registerSettingsPage(id, contribution)` | `ui.settings` | Adds a plugin settings page. | +| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` | Adds a side panel. | +| `ui.registerChannelSection(id, contribution)` | `ui.channelsSection` | Adds a channel section. | +| `ui.registerComposerAction(id, contribution)` | `ui.pages` | Adds a composer action. | +| `ui.registerProfileAction(id, contribution)` | `ui.pages` | Adds a profile action. | +| `ui.registerToolbarAction(id, contribution)` | `ui.pages` | Adds a toolbar action. | +| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` | Adds an embed renderer. | +| `ui.mountElement(id, request)` | `ui.dom` | Mounts plugin-owned DOM into a target element or selector. | + +## Context and Logger + +| Method | Capability | Description | +| --- | --- | --- | +| `context.getCurrent()` | None | Reads current user, server, active text channel, and active voice channel. | +| `logger.debug(message, data?)` | None | Writes a debug plugin log entry. | +| `logger.info(message, data?)` | None | Writes an info plugin log entry. | +| `logger.warn(message, data?)` | None | Writes a warning plugin log entry. | +| `logger.error(message, data?)` | None | Writes an error plugin log entry. | diff --git a/docs-site/docs/plugin-development/api/channels.md b/docs-site/docs/plugin-development/api/channels.md new file mode 100644 index 0000000..5e29a2d --- /dev/null +++ b/docs-site/docs/plugin-development/api/channels.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 5 +--- + +# Channels API + +The channels API reads, selects, creates, renames, and removes server channels. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `channels.list()` | `channels.read` | +| `channels.select(channelId)` | `channels.read` | +| `channels.addAudioChannel(request)` | `channels.manage` | +| `channels.addVideoChannel(request)` | `channels.manage` | +| `channels.rename(channelId, name)` | `channels.manage` | +| `channels.remove(channelId)` | `channels.manage` | + +## List Channels + +```js +export function activate(context) { + const channels = context.api.channels.list(); + + context.api.logger.info('Channels', channels.map((channel) => ({ + id: channel.id, + name: channel.name, + type: channel.type + }))); +} +``` + +Example channel list: + +```json +[ + { "id": "general", "name": "general", "type": "text", "position": 0 }, + { "id": "support", "name": "support", "type": "text", "position": 1 }, + { "id": "lobby", "name": "Lobby", "type": "audio", "position": 10 } +] +``` + +## Select a Channel + +```js +export function activate(context) { + context.api.channels.select('support'); +} +``` + +## Add a Voice Channel + +```js +export function activate(context) { + context.api.channels.addAudioChannel({ + id: 'raid-voice', + name: 'Raid Voice', + position: 20 + }); +} +``` + +## Add a Video Channel Section + +```js +export function activate(context) { + context.api.channels.addVideoChannel({ + id: 'watch-party-video', + name: 'Watch Party', + position: 30 + }); +} +``` + +## Rename and Remove + +```js +export function activate(context) { + context.api.channels.rename('raid-voice', 'Raid Voice - Tonight'); + context.api.channels.remove('old-event-room'); +} +``` + +Channel creation, rename, and removal should be user-confirmed because they change the shared server structure. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/context-and-logging.md b/docs-site/docs/plugin-development/api/context-and-logging.md new file mode 100644 index 0000000..d8b8afd --- /dev/null +++ b/docs-site/docs/plugin-development/api/context-and-logging.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 1 +--- + +# Context and Logging + +Context and logging are available to every plugin. They do not require privileged capabilities. + +## context.getCurrent() + +Reads the current interaction context. + +```js +export function activate(context) { + const current = context.api.context.getCurrent(); + + context.api.logger.info('Current context', { + serverName: current.server?.name ?? 'No server open', + textChannel: current.textChannel?.name ?? 'No text channel selected', + voiceChannel: current.voiceChannel?.name ?? 'Not connected to voice', + user: current.user?.displayName ?? 'No user' + }); +} +``` + +Example context shape: + +```json +{ + "source": "manual", + "server": { "id": "room-7ebdde75", "name": "Friday Game Night" }, + "textChannel": { "id": "general", "name": "general", "type": "text" }, + "voiceChannel": { "id": "lobby", "name": "Lobby", "type": "audio" }, + "user": { "id": "user-alice-01", "displayName": "Alice" } +} +``` + +## Action Context + +Composer, toolbar, and profile actions receive context directly. + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerToolbarAction('where-am-i', { + label: 'Where am I?', + run: (actionContext) => { + context.api.logger.info('Toolbar action context', { + source: actionContext.source, + serverId: actionContext.server?.id, + textChannelId: actionContext.textChannel?.id, + voiceChannelId: actionContext.voiceChannel?.id + }); + } + })); +} +``` + +Capability required: `ui.pages` for the toolbar action. The context object itself needs no extra capability. + +## Logger Methods + +```js +export function activate(context) { + const { logger } = context.api; + + logger.debug('Preparing plugin', { pluginId: context.pluginId }); + logger.info('Plugin activated', { version: context.manifest.version }); + logger.warn('Optional service unavailable', { service: 'weather.example.com' }); + logger.error('Failed to parse saved preference', { key: 'soundboard:favorites' }); +} +``` + +Logs are visible in the Plugin Manager. Avoid logging passwords, bearer tokens, or private message contents. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/events.md b/docs-site/docs/plugin-development/api/events.md new file mode 100644 index 0000000..2eded9d --- /dev/null +++ b/docs-site/docs/plugin-development/api/events.md @@ -0,0 +1,100 @@ +--- +sidebar_position: 7 +--- + +# Events API + +Plugin events allow plugins to publish and subscribe to declared server or P2P events. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `events.publishServer(eventName, payload)` | `events.server.publish` | +| `events.subscribeServer(subscription)` | `events.server.subscribe` | +| `events.publishP2p(eventName, payload)` | `events.p2p.publish` | +| `events.subscribeP2p(subscription)` | `events.p2p.subscribe` | + +## Declare Events in the Manifest + +```json +{ + "events": [ + { + "eventName": "poll:vote", + "direction": "p2pHint", + "scope": "channel", + "maxPayloadBytes": 2048 + }, + { + "eventName": "moderation:flag", + "direction": "serverRelay", + "scope": "server", + "maxPayloadBytes": 4096 + } + ] +} +``` + +## Publish and Subscribe to P2P Events + +```js +export function activate(context) { + context.subscriptions.push(context.api.events.subscribeP2p({ + eventName: 'poll:vote', + handler: (event) => { + context.api.logger.info('Vote received', { + optionId: event.payload?.optionId, + voterName: event.payload?.voterName, + eventId: event.eventId + }); + } + })); + + context.api.events.publishP2p('poll:vote', { + pollId: 'raid-night-2026-04-29', + optionId: 'dungeon', + voterName: 'Alice' + }); +} +``` + +## Publish and Subscribe to Server Events + +```js +export function activate(context) { + context.subscriptions.push(context.api.events.subscribeServer({ + eventName: 'moderation:flag', + handler: (event) => { + context.api.logger.warn('Moderation flag received', { + messageId: event.payload?.messageId, + reason: event.payload?.reason + }); + } + })); + + context.api.events.publishServer('moderation:flag', { + messageId: 'msg-20260429-flagged', + reason: 'Possible spam link', + reportedBy: 'user-alice-01' + }); +} +``` + +Example event envelope: + +```json +{ + "type": "plugin_event", + "eventName": "poll:vote", + "pluginId": "example.polls", + "serverId": "room-7ebdde75", + "eventId": "event-1777473600000-1", + "emittedAt": 1777473600000, + "payload": { + "pollId": "raid-night-2026-04-29", + "optionId": "dungeon", + "voterName": "Alice" + } +} +``` \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/message-bus.md b/docs-site/docs/plugin-development/api/message-bus.md new file mode 100644 index 0000000..da8138e --- /dev/null +++ b/docs-site/docs/plugin-development/api/message-bus.md @@ -0,0 +1,95 @@ +--- +sidebar_position: 8 +--- + +# Message Bus API + +The plugin message bus sends plugin-only P2P events. It can also include bounded latest-message snapshots for plugins that coordinate around recent chat state. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `messageBus.publish(request)` | `events.p2p.publish`, plus `messages.read` if `includeLatestMessages` is true | +| `messageBus.sendLatestMessages(request?)` | `events.p2p.publish` and `messages.read` | +| `messageBus.subscribe(subscription)` | `events.p2p.subscribe`, plus `messages.read` if replaying latest messages | + +## Subscribe + +```js +export function activate(context) { + context.subscriptions.push(context.api.messageBus.subscribe({ + topic: 'poll:votes', + channelId: 'general', + replayLatest: true, + latestMessageLimit: 10, + handler: (event) => { + context.api.logger.info('Poll bus event', { + topic: event.topic, + choice: event.payload?.choice, + messageCount: event.messages?.length ?? 0 + }); + } + })); +} +``` + +## Publish + +```js +export function activate(context) { + const envelope = context.api.messageBus.publish({ + topic: 'poll:votes', + channelId: 'general', + payload: { + pollId: 'raid-night-2026-04-29', + choice: 'healer', + voter: 'Alice' + }, + includeLatestMessages: true, + includeSelf: true, + latestMessageLimit: 10, + sinceTimestamp: 1777470000000 + }); + + context.api.logger.info('Published poll event', { eventId: envelope.eventId }); +} +``` + +Example envelope: + +```json +{ + "eventId": "plugin-bus-1777473600000-1", + "pluginId": "example.polls", + "roomId": "room-7ebdde75", + "channelId": "general", + "topic": "poll:votes", + "sentAt": 1777473600000, + "payload": { + "pollId": "raid-night-2026-04-29", + "choice": "healer", + "voter": "Alice" + }, + "messages": [ + { "id": "msg-1", "content": "Raid tonight?", "channelId": "general" } + ] +} +``` + +## Send Latest Messages + +```js +export function activate(context) { + context.api.messageBus.sendLatestMessages({ + topic: 'chat:snapshot', + channelId: 'support', + limit: 25, + includeDeleted: false, + sinceTimestamp: 1777460000000, + targetPeerId: 'peer-muse-laptop' + }); +} +``` + +Use the message bus for plugin coordination. Do not use it for normal user chat messages; use `messages.send()` for that. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/messages-and-typing.md b/docs-site/docs/plugin-development/api/messages-and-typing.md new file mode 100644 index 0000000..dcb93ad --- /dev/null +++ b/docs-site/docs/plugin-development/api/messages-and-typing.md @@ -0,0 +1,144 @@ +--- +sidebar_position: 6 +--- + +# Messages and Typing API + +The messages API reads current messages, sends messages, edits or deletes plugin-owned messages, moderates messages, syncs messages, and exposes typing state. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `messages.readCurrent()` | `messages.read` | +| `messages.send(content, channelId?)` | `messages.send` | +| `messages.sendAsPluginUser(request)` | `messages.send` | +| `messages.setTyping(isTyping, channelId?)` | `messages.send` | +| `messages.subscribeTyping(handler)` | `messages.read` | +| `messages.edit(messageId, content)` | `messages.editOwn` | +| `messages.delete(messageId)` | `messages.deleteOwn` | +| `messages.moderateDelete(messageId)` | `messages.moderate` | +| `messages.sync(messages)` | `messages.sync` | + +## Read Current Messages + +```js +export function activate(context) { + const messages = context.api.messages.readCurrent(); + + context.api.logger.info('Current messages', messages.slice(-3).map((message) => ({ + id: message.id, + channelId: message.channelId, + senderName: message.senderName, + content: message.content + }))); +} +``` + +## Send a Message + +```js +export function activate(context) { + const created = context.api.messages.send( + 'Reminder: raid starts at 20:00. Bring repairs and snacks.', + 'general' + ); + + context.api.logger.info('Sent reminder', { messageId: created.id }); +} +``` + +## Send as a Plugin User + +```js +export function activate(context) { + const botUserId = context.api.server.registerPluginUser({ + id: 'poll-bot', + displayName: 'Poll Bot' + }); + + context.api.messages.sendAsPluginUser({ + pluginUserId: botUserId, + channelId: 'general', + content: 'Poll is open: react with 1 for dungeon, 2 for arena, 3 for crafting.' + }); +} +``` + +Capabilities required: `users.manage` and `messages.send`. + +## Edit and Delete Plugin-Owned Messages + +```js +export function activate(context) { + const message = context.api.messages.send('Draft event reminder', 'announcements'); + + context.api.messages.edit(message.id, 'Event reminder: voice meetup starts in 15 minutes.'); + context.api.messages.delete(message.id); +} +``` + +## Moderation Delete + +```js +export function activate(context) { + context.api.messages.moderateDelete('msg-spam-20260429-001'); +} +``` + +Use moderation from explicit moderator actions, not automatic activation. + +## Typing State + +```js +export function activate(context) { + context.api.messages.setTyping(true, 'general'); + + setTimeout(() => { + context.api.messages.setTyping(false, 'general'); + }, 1500); + + context.subscriptions.push(context.api.messages.subscribeTyping((event) => { + context.api.logger.info('Typing event', { + displayName: event.displayName, + isTyping: event.isTyping, + channelId: event.channelId, + serverId: event.serverId, + voiceChannel: event.voiceChannel?.name ?? null + }); + })); +} +``` + +Example typing event: + +```json +{ + "serverId": "room-7ebdde75", + "channelId": "general", + "userId": "user-muse-01", + "displayName": "Muse", + "isTyping": true +} +``` + +## Sync Messages + +```js +export function activate(context) { + context.api.messages.sync([ + { + id: 'external-standup-001', + roomId: 'room-7ebdde75', + channelId: 'standup', + senderId: 'standup-importer', + senderName: 'Standup Importer', + content: 'Imported note: Alice is working on plugin docs.', + timestamp: 1777473600000, + isDeleted: false + } + ]); +} +``` + +Sync should preserve message ids and timestamps from the source system when possible. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/p2p-and-media.md b/docs-site/docs/plugin-development/api/p2p-and-media.md new file mode 100644 index 0000000..fcc3a3a --- /dev/null +++ b/docs-site/docs/plugin-development/api/p2p-and-media.md @@ -0,0 +1,128 @@ +--- +sidebar_position: 9 +--- + +# P2P and Media API + +P2P APIs send plugin data to connected peers. Media APIs play audio and contribute custom streams. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `p2p.connectedPeers()` | `p2p.data` | +| `p2p.broadcastData(eventName, payload)` | `p2p.data` | +| `p2p.sendData(peerId, eventName, payload)` | `p2p.data` | +| `media.playAudioClip(request)` | `media.playAudio` | +| `media.addCustomAudioStream(request)` | `media.addAudioStream` | +| `media.addCustomVideoStream(request)` | `media.addVideoStream` | +| `media.setInputVolume(volume)` | `audio.volume` | +| `media.setOutputVolume(volume)` | `audio.volume` | + +## Connected Peers + +```js +export function activate(context) { + const peerIds = context.api.p2p.connectedPeers(); + + context.api.logger.info('Connected peers', { peerIds }); +} +``` + +## Broadcast Data + +```js +export function activate(context) { + context.api.p2p.broadcastData('soundboard:played', { + soundId: 'airhorn-short', + label: 'Airhorn', + playedBy: 'Alice', + playedAt: 1777473600000 + }); +} +``` + +## Send Data to One Peer + +```js +export function activate(context) { + context.api.p2p.sendData('peer-muse-laptop', 'private-tool:ping', { + requestId: 'ping-20260429-001', + message: 'Are you receiving plugin data?' + }); +} +``` + +## Play an Audio Clip + +```js +export async function activate(context) { + await context.api.media.playAudioClip({ + url: 'https://cdn.example.com/metoyou/sounds/chime.wav', + volume: 0.65 + }); +} +``` + +## Add a Custom Audio Stream + +```js +export async function activate(context) { + const audioContext = new AudioContext(); + const oscillator = audioContext.createOscillator(); + const gain = audioContext.createGain(); + const destination = audioContext.createMediaStreamDestination(); + + oscillator.type = 'sine'; + oscillator.frequency.value = 440; + gain.gain.value = 0.03; + oscillator.connect(gain); + gain.connect(destination); + oscillator.start(); + + await context.api.media.addCustomAudioStream({ + label: 'Tuning tone', + stream: destination.stream + }); + + setTimeout(async () => { + oscillator.stop(); + await audioContext.close(); + }, 1000); +} +``` + +## Add a Custom Video Stream + +```js +export async function activate(context) { + const canvas = document.createElement('canvas'); + canvas.width = 1280; + canvas.height = 720; + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#111827'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#ffffff'; + ctx.font = '48px sans-serif'; + ctx.fillText('Plugin camera scene', 80, 120); + + const stream = canvas.captureStream(15); + + await context.api.media.addCustomVideoStream({ + label: 'Plugin camera scene', + stream + }); +} +``` + +## Set Volumes + +```js +export function activate(context) { + context.api.media.setInputVolume(0.85); + context.api.media.setOutputVolume(0.75); +} +``` + +Use media APIs with visible controls and clear user consent. Unexpected audio or video is a poor user experience. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/profile.md b/docs-site/docs/plugin-development/api/profile.md new file mode 100644 index 0000000..e3842aa --- /dev/null +++ b/docs-site/docs/plugin-development/api/profile.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 2 +--- + +# Profile API + +The profile API reads and updates the current user's local profile details. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `profile.getCurrent()` | `profile.read` | +| `profile.update(profile)` | `profile.write` | +| `profile.updateAvatar(avatar)` | `profile.write` | + +## Read Current Profile + +```js +export function activate(context) { + const user = context.api.profile.getCurrent(); + + context.api.logger.info('Current profile', { + id: user?.id, + displayName: user?.displayName, + username: user?.username + }); +} +``` + +Example result: + +```json +{ + "id": "user-alice-01", + "username": "alice", + "displayName": "Alice", + "description": "Raids on Fridays", + "avatarUrl": "/avatars/alice.webp" +} +``` + +## Update Display Profile + +```js +export function activate(context) { + context.api.profile.update({ + displayName: 'Alice - Support Lead', + description: 'Available for onboarding and support questions.' + }); +} +``` + +## Update Avatar + +```js +export function activate(context) { + context.api.profile.updateAvatar({ + avatarUrl: 'https://cdn.example.com/metoyou/avatars/alice-support.png', + avatarMime: 'image/png', + avatarHash: 'sha256:9df5d5e4b0d8f41f3a3cf5d1f5a2c1f4' + }); +} +``` + +Use `profile.write` carefully. A plugin that changes a user's identity should explain why in its readme and UI. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/server.md b/docs-site/docs/plugin-development/api/server.md new file mode 100644 index 0000000..bf29429 --- /dev/null +++ b/docs-site/docs/plugin-development/api/server.md @@ -0,0 +1,81 @@ +--- +sidebar_position: 4 +--- + +# Server API + +The server API reads the active server, registers plugin-owned users, and updates server settings or permissions. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `server.getCurrent()` | `server.read` | +| `server.registerPluginUser(request)` | `users.manage` | +| `server.updatePermissions(permissions)` | `server.manage` | +| `server.updateSettings(settings)` | `server.manage` | + +## Read Current Server + +```js +export function activate(context) { + const server = context.api.server.getCurrent(); + + context.api.logger.info('Current server', { + id: server?.id, + name: server?.name, + topic: server?.topic, + isPrivate: server?.isPrivate + }); +} +``` + +## Register a Plugin User + +Plugin users are useful for bot-style messages. + +```js +export function activate(context) { + const botUserId = context.api.server.registerPluginUser({ + id: 'standup-helper-bot', + displayName: 'Standup Helper', + avatarUrl: 'https://cdn.example.com/metoyou/plugins/standup-helper.png' + }); + + context.api.messages.sendAsPluginUser({ + pluginUserId: botUserId, + channelId: 'general', + content: 'Standup reminder: share yesterday, today, and blockers.' + }); +} +``` + +Capabilities required: `users.manage` and `messages.send`. + +## Update Server Settings + +```js +export function activate(context) { + context.api.server.updateSettings({ + name: 'Friday Game Night', + topic: 'Co-op games, voice chat, and clips', + description: 'A friendly server for Friday sessions.', + maxUsers: 64, + isPrivate: false + }); +} +``` + +## Update Permissions + +```js +export function activate(context) { + context.api.server.updatePermissions({ + allowVoice: true, + allowVideo: true, + allowScreenShare: true + }); +} +``` + +Only update settings or permissions as part of an explicit admin flow. Plugins should not silently rename servers or change access rules. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/storage.md b/docs-site/docs/plugin-development/api/storage.md new file mode 100644 index 0000000..384ced9 --- /dev/null +++ b/docs-site/docs/plugin-development/api/storage.md @@ -0,0 +1,101 @@ +--- +sidebar_position: 10 +--- + +# Storage API + +Plugins can store local client data and per-server data. Desktop builds use Electron persistence when available; browser fallback uses renderer storage. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `clientData.read(key)` | `storage.local` | +| `clientData.write(key, value)` | `storage.local` | +| `clientData.remove(key)` | `storage.local` | +| `serverData.read(key)` | `storage.serverData.read` | +| `serverData.write(key, value)` | `storage.serverData.write` | +| `serverData.remove(key)` | `storage.serverData.write` | +| `storage.get(key)` | `storage.local` | +| `storage.set(key, value)` | `storage.local` | +| `storage.remove(key)` | `storage.local` | + +## Client Data + +Client data belongs to this local user and client. + +```js +export async function activate(context) { + await context.api.clientData.write('soundboard:volume', { + masterVolume: 0.7, + updatedAt: 1777473600000 + }); + + const value = await context.api.clientData.read('soundboard:volume'); + + context.api.logger.info('Loaded client data', value); +} +``` + +## Server Data + +Server data is local per-user/per-server state. It is not arbitrary signal-server persistence. + +```js +export async function activate(context) { + await context.api.serverData.write('soundboard:favorites', [ + { id: 'chime', label: 'Chime', url: 'https://cdn.example.com/chime.wav' }, + { id: 'ready', label: 'Ready Check', url: 'https://cdn.example.com/ready.wav' } + ]); + + const favorites = await context.api.serverData.read('soundboard:favorites'); + + context.api.logger.info('Loaded server favorites', favorites); +} +``` + +## Remove Data + +```js +export async function activate(context) { + await context.api.clientData.remove('soundboard:volume'); + await context.api.serverData.remove('soundboard:favorites'); +} +``` + +## Legacy Synchronous Storage + +The `storage.*` methods are legacy local storage helpers. Prefer `clientData.*` for new plugins when async reads are acceptable. + +```js +export function activate(context) { + context.api.storage.set('quick-toggle', { enabled: true }); + + const saved = context.api.storage.get('quick-toggle'); + + context.api.logger.info('Legacy storage value', saved); + + context.api.storage.remove('quick-toggle'); +} +``` + +## Manifest Data Declarations + +Declare important data keys in the manifest. + +```json +{ + "data": [ + { + "key": "soundboard:volume", + "scope": "client", + "storage": "local" + }, + { + "key": "soundboard:favorites", + "scope": "server", + "storage": "serverData" + } + ] +} +``` \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/ui.md b/docs-site/docs/plugin-development/api/ui.md new file mode 100644 index 0000000..1265868 --- /dev/null +++ b/docs-site/docs/plugin-development/api/ui.md @@ -0,0 +1,235 @@ +--- +sidebar_position: 11 +--- + +# UI API + +The UI API lets plugins add pages, settings pages, side panels, channel sections, actions, embed renderers, and controlled DOM mounts. + +Prefer registered UI contributions over direct DOM mounting. Contribution APIs let Angular render the plugin UI when the matching app surface exists. Direct DOM mounting runs immediately and throws if the target selector is not present. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `ui.registerAppPage(id, contribution)` | `ui.pages` | +| `ui.registerSettingsPage(id, contribution)` | `ui.settings` | +| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` | +| `ui.registerChannelSection(id, contribution)` | `ui.channelsSection` | +| `ui.registerComposerAction(id, contribution)` | `ui.pages` | +| `ui.registerProfileAction(id, contribution)` | `ui.pages` | +| `ui.registerToolbarAction(id, contribution)` | `ui.pages` | +| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` | +| `ui.mountElement(id, request)` | `ui.dom` | + +Every registration returns a disposable. Push it into `context.subscriptions`. + +## App Page + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerAppPage('dashboard', { + label: 'Raid Dashboard', + path: '/plugins/example.raid-helper/dashboard', + render: () => { + const root = document.createElement('section'); + root.innerHTML = '

Raid Dashboard

Tonight: dungeon practice.

'; + return root; + } + })); +} +``` + +The page is hosted by `/plugins/:pluginId/:pageId`. + +## Settings Page + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerSettingsPage('preferences', { + label: 'Raid Helper', + settingsKey: 'raid-helper', + order: 20, + render: () => { + const wrapper = document.createElement('section'); + const label = document.createElement('label'); + const checkbox = document.createElement('input'); + + checkbox.type = 'checkbox'; + checkbox.checked = true; + label.append(checkbox, ' Enable ready-check reminders'); + wrapper.append(label); + return wrapper; + } + })); +} +``` + +## Side Panel + +Use `ui.registerSidePanel` for content that belongs in the server sidebar plugin area. Do not mount directly into `[data-testid="plugin-room-side-panel"]`; that host is route-specific and may not exist during plugin activation. + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerSidePanel('soundboard', { + label: 'Soundboard', + order: 10, + render: () => { + const panel = document.createElement('div'); + const button = document.createElement('button'); + + button.type = 'button'; + button.textContent = 'Play chime'; + button.onclick = () => context.api.media.playAudioClip({ + url: 'https://cdn.example.com/chime.wav', + volume: 0.6 + }); + panel.append(button); + return panel; + } + })); +} +``` + +Capabilities required: `ui.sidePanel` and `media.playAudio`. + +## Channel Section + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerChannelSection('events', { + label: 'Event Rooms', + type: 'custom', + order: 50 + })); +} +``` + +## Composer Action + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerComposerAction('insert-standup', { + icon: 'ST', + label: 'Insert standup prompt', + run: (actionContext) => { + context.api.messages.send( + 'Standup: yesterday I..., today I..., blocked by...', + actionContext.textChannel?.id + ); + } + })); +} +``` + +Capabilities required: `ui.pages` and `messages.send`. + +## Profile Action + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerProfileAction('wave', { + label: 'Wave', + run: (actionContext) => { + context.api.messages.send(`Waving at ${actionContext.user?.displayName ?? 'someone'}!`); + } + })); +} +``` + +## Toolbar Action + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerToolbarAction('open-dashboard', { + label: 'Raid Helper', + run: () => { + context.api.logger.info('Open the Raid Helper plugin page from /plugins/example.raid-helper/dashboard'); + } + })); +} +``` + +## Embed Renderer + +```js +export function activate(context) { + context.subscriptions.push(context.api.ui.registerEmbedRenderer('raid-card', { + embedType: 'raid.card', + render: (payload) => { + const card = document.createElement('article'); + const title = document.createElement('h3'); + const body = document.createElement('p'); + + title.textContent = payload?.title ?? 'Raid'; + body.textContent = payload?.description ?? 'No description provided.'; + card.append(title, body); + return card; + } + })); +} +``` + +Example message content for this embed: + +```text +toju:embed:raid.card:{"title":"Friday Raid","description":"Meet in Lobby at 20:00."} +``` + +## DOM Mount + +Use DOM mounting only when normal UI contribution points are not enough. `ui.mountElement` resolves its target immediately. If the target does not exist, plugin activation fails with `Plugin mount target not found: `. + +Safe uses: + +- Mounting a global overlay, badge, or modal into `body` during activation. +- Mounting into a route-specific element only after checking that element exists. + +Avoid: + +- Mounting sidebar content into `[data-testid="plugin-room-side-panel"]`. Use `ui.registerSidePanel`. +- Mounting chat content into `app-chat-messages` during activation without checking for the element. + +```js +export function activate(context) { + const badge = document.createElement('div'); + badge.textContent = 'Raid helper active'; + badge.style.position = 'fixed'; + badge.style.right = '16px'; + badge.style.bottom = '16px'; + badge.style.padding = '8px 10px'; + badge.style.background = '#111827'; + badge.style.color = 'white'; + badge.style.borderRadius = '6px'; + + context.subscriptions.push(context.api.ui.mountElement('active-badge', { + target: 'body', + position: 'beforeend', + element: badge + })); +} +``` + +Route-specific mount example with a guard: + +```js +export function activate(context) { + const target = document.querySelector('app-chat-messages'); + + if (!target) { + context.api.logger.warn('Chat messages host is not rendered yet; skipping chat mount'); + return; + } + + const banner = document.createElement('div'); + banner.textContent = 'Raid helper active in this chat.'; + + context.subscriptions.push(context.api.ui.mountElement('chat-banner', { + target, + position: 'afterbegin', + element: banner + })); +} +``` + +The runtime tags plugin-owned DOM and removes it on unload, but plugins should still keep mounts minimal and accessible. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/api/users-and-roles.md b/docs-site/docs/plugin-development/api/users-and-roles.md new file mode 100644 index 0000000..e2bdeb5 --- /dev/null +++ b/docs-site/docs/plugin-development/api/users-and-roles.md @@ -0,0 +1,89 @@ +--- +sidebar_position: 3 +--- + +# Users and Roles API + +The users and roles APIs read known users, read room members, and perform moderation or role changes when granted. + +## Required Capabilities + +| Method | Capability | +| --- | --- | +| `users.getCurrent()` | `users.read` | +| `users.list()` | `users.read` | +| `users.readMembers()` | `users.read` | +| `users.setRole(userId, role)` | `roles.manage` | +| `users.kick(userId)` | `users.manage` | +| `users.ban(userId, reason?)` | `users.manage` | +| `roles.list()` | `roles.read` | +| `roles.setAssignments(assignments)` | `roles.manage` | + +## Read Users + +```js +export function activate(context) { + const currentUser = context.api.users.getCurrent(); + const knownUsers = context.api.users.list(); + const roomMembers = context.api.users.readMembers(); + + context.api.logger.info('Room user summary', { + currentUser: currentUser?.displayName, + knownUserCount: knownUsers.length, + memberCount: roomMembers.length + }); +} +``` + +Example member data: + +```json +[ + { "id": "member-1", "userId": "user-alice-01", "displayName": "Alice", "role": "admin" }, + { "id": "member-2", "userId": "user-muse-01", "displayName": "Muse", "role": "member" } +] +``` + +## Read Roles + +```js +export function activate(context) { + const roles = context.api.roles.list(); + + context.api.logger.info('Available roles', roles.map((role) => ({ + id: role.id, + name: role.name, + permissions: role.permissions + }))); +} +``` + +## Set a User Role + +```js +export function activate(context) { + context.api.users.setRole('user-muse-01', 'moderator'); +} +``` + +## Replace Role Assignments + +```js +export function activate(context) { + context.api.roles.setAssignments([ + { userId: 'user-alice-01', roleId: 'admin' }, + { userId: 'user-muse-01', roleId: 'moderator' } + ]); +} +``` + +## Kick or Ban a User + +```js +export function activate(context) { + context.api.users.kick('user-spam-01'); + context.api.users.ban('user-spam-02', 'Repeated spam in support channels'); +} +``` + +Moderation calls should normally be behind an explicit user action in plugin UI. Do not run destructive moderation automatically on activation. \ No newline at end of file diff --git a/docs-site/docs/plugin-development/capabilities.md b/docs-site/docs/plugin-development/capabilities.md new file mode 100644 index 0000000..b723757 --- /dev/null +++ b/docs-site/docs/plugin-development/capabilities.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 3 +--- + +# Capabilities + +Capabilities protect privileged app surfaces. A plugin must declare a capability in its manifest and the user must grant it before the runtime allows the corresponding API call. + +| Capability | API areas | Notes | +| --- | --- | --- | +| `profile.read` | `profile.getCurrent()` | Reads the current user. | +| `profile.write` | `profile.update()`, `profile.updateAvatar()` | Updates local profile fields and avatar metadata. | +| `users.read` | `users.getCurrent()`, `users.list()`, `users.readMembers()` | Reads users and server members. | +| `users.manage` | `users.kick()`, `users.ban()`, `server.registerPluginUser()` | Can create plugin users and moderate members. | +| `roles.read` | `roles.list()` | Reads server roles. | +| `roles.manage` | `roles.setAssignments()`, `users.setRole()` | Changes role assignments or user roles. | +| `messages.read` | `messages.readCurrent()`, message bus latest snapshots | Reads current channel messages. | +| `messages.send` | `messages.send()`, `messages.sendAsPluginUser()` | Sends messages as the current user or registered plugin user. | +| `messages.editOwn` | `messages.edit()` | Edits plugin-owned messages. | +| `messages.deleteOwn` | `messages.delete()` | Deletes plugin-owned messages. | +| `messages.moderate` | `messages.moderateDelete()` | Moderation delete path. | +| `messages.sync` | `messages.sync()` | Syncs message arrays into client state. | +| `channels.read` | `channels.list()`, `channels.select()` | Reads and selects channels. | +| `channels.manage` | `channels.addAudioChannel()`, `channels.addVideoChannel()`, `channels.remove()`, `channels.rename()` | Mutates channel or channel-section state. | +| `server.read` | `server.getCurrent()` | Reads active server. | +| `server.manage` | `server.updatePermissions()`, `server.updateSettings()` | Updates server permissions or settings. | +| `p2p.data` | `p2p.connectedPeers()`, `p2p.broadcastData()`, `p2p.sendData()` | Uses plugin peer data paths. | +| `p2p.media` | Reserved peer media features. | Included for media-facing plugins. | +| `media.playAudio` | `media.playAudioClip()` | Plays an audio URL locally. | +| `media.addAudioStream` | `media.addCustomAudioStream()` | Adds a custom stream to voice handling. | +| `media.addVideoStream` | `media.addCustomVideoStream()` | Registers custom video stream contribution. | +| `audio.volume` | `media.setInputVolume()`, `media.setOutputVolume()` | Adjusts local voice volume. | +| `audio.effects` | Reserved audio effect features. | Included for audio processing plugins. | +| `ui.settings` | `ui.registerSettingsPage()` | Adds settings pages. | +| `ui.pages` | `ui.registerAppPage()`, `ui.registerComposerAction()`, `ui.registerProfileAction()`, `ui.registerToolbarAction()` | Adds app pages and actions. | +| `ui.sidePanel` | `ui.registerSidePanel()` | Adds side panels. | +| `ui.channelsSection` | `ui.registerChannelSection()` | Adds channel sections. | +| `ui.embeds` | `ui.registerEmbedRenderer()` | Renders custom embeds. | +| `ui.dom` | `ui.mountElement()` | Mounts plugin-owned DOM into app targets. | +| `storage.local` | `storage.*`, `clientData.*` | Reads and writes plugin-local data. | +| `storage.serverData.read` | `serverData.read()` | Reads local per-user/per-server plugin data. | +| `storage.serverData.write` | `serverData.write()`, `serverData.remove()` | Writes or removes local per-user/per-server plugin data. | +| `events.server.publish` | `events.publishServer()` | Publishes declared server plugin events. | +| `events.server.subscribe` | `events.subscribeServer()` | Subscribes to declared server plugin events. | +| `events.p2p.publish` | `events.publishP2p()`, `messageBus.publish()`, `messageBus.sendLatestMessages()` | Publishes declared P2P/plugin bus events. | +| `events.p2p.subscribe` | `events.subscribeP2p()`, `messageBus.subscribe()` | Subscribes to declared P2P/plugin bus events. | + +## Recommended Practice + +Request the fewest capabilities possible. Separate broad features into optional plugin modules when a single plugin would otherwise need many unrelated grants. diff --git a/docs-site/docs/plugin-development/create-a-plugin.md b/docs-site/docs/plugin-development/create-a-plugin.md new file mode 100644 index 0000000..d881012 --- /dev/null +++ b/docs-site/docs/plugin-development/create-a-plugin.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 1 +--- + +# Create a Plugin + +MetoYou plugins are browser-safe ES modules loaded by the Angular renderer. A plugin receives a frozen `TojuClientPluginApi`, declares every privileged capability in its manifest, and registers cleanup work through disposables. + +## Folder Layout + +A local desktop plugin is discovered from an immediate child folder under the app data `plugins` directory. + +```text +my-plugin/ + toju-plugin.json + main.js + README.md + icon.svg +``` + +The manifest file can be named `toju-plugin.json` or `plugin.json`. Entrypoints and readmes must stay inside the plugin folder. + +## Minimal Manifest + +```json +{ + "schemaVersion": 1, + "id": "example.hello-world", + "title": "Hello World", + "description": "Adds a toolbar action that sends a message.", + "version": "1.0.0", + "kind": "client", + "scope": "client", + "apiVersion": "1.0.0", + "compatibility": { + "minimumTojuVersion": "1.0.0" + }, + "entrypoint": "./main.js", + "capabilities": ["messages.send", "ui.pages"] +} +``` + +## Entrypoint + +```js +export function activate(context) { + const { api } = context; + + api.logger.info('Hello World activated'); + + const disposable = api.ui.registerToolbarAction('hello', { + label: 'Hello', + run: () => api.messages.send('Hello from my plugin') + }); + + context.subscriptions.push(disposable); +} + +export function ready(context) { + context.api.logger.info('All ready plugins have loaded'); +} + +export function deactivate(context) { + context.api.logger.info('Hello World deactivated'); +} +``` + +## Lifecycle Hooks + +| Hook | When it runs | Use it for | +| --- | --- | --- | +| `activate(context)` | During explicit plugin activation. | Register UI, subscribe to events, initialize state. | +| `ready(context)` | After the load-order pass has activated ready plugins. | Cross-plugin coordination that needs other plugins loaded. | +| `deactivate(context)` | During unload or reload. | Flush state and log shutdown. Disposables are also cleaned up by the host. | +| `onPluginDataChanged(context, event)` | When plugin data changes are observed. | React to plugin-scoped persistence changes. | +| `onServerRequirementsChanged(context, snapshot)` | When server plugin requirements change. | Adapt to required, optional, blocked, or incompatible server plugins. | + +## Cleanup + +Every API registration returns a disposable. Push it into `context.subscriptions`. + +```js +const subscription = api.messageBus.subscribe({ + topic: 'poll:votes', + handler: (event) => api.logger.info('vote received', event.payload) +}); + +context.subscriptions.push(subscription); +``` + +The plugin host disposes subscriptions in reverse order when the plugin unloads. + +## Capability Grants + +A plugin can only call privileged APIs after the matching capability is declared in the manifest and granted by the user. Keep the manifest narrow. For example, a plugin that only adds a settings page does not need message or user management capabilities. + +## Testing Locally + +1. Create the plugin folder in the desktop plugins directory. +2. Open the Plugin Manager. +3. Register or refresh local plugins. +4. Grant required capabilities. +5. Activate the plugin. +6. Inspect plugin logs in the manager. + +For broad API examples, compare against the E2E fixture plugin under `toju-app/public/plugins/e2e-all-api/`. diff --git a/docs-site/docs/plugin-development/examples.md b/docs-site/docs/plugin-development/examples.md new file mode 100644 index 0000000..ec93a08 --- /dev/null +++ b/docs-site/docs/plugin-development/examples.md @@ -0,0 +1,204 @@ +--- +sidebar_position: 5 +--- + +# Examples + +## Toolbar Message Plugin + +`toju-plugin.json` + +```json +{ + "schemaVersion": 1, + "id": "example.toolbar-message", + "title": "Toolbar Message", + "description": "Adds a toolbar action that sends a reusable message.", + "version": "1.0.0", + "kind": "client", + "scope": "client", + "apiVersion": "1.0.0", + "compatibility": { + "minimumTojuVersion": "1.0.0", + "verifiedTojuVersion": "1.0.0" + }, + "entrypoint": "./main.js", + "capabilities": ["messages.send", "ui.pages"] +} +``` + +`main.js` + +```js +export function activate(context) { + const { api } = context; + + context.subscriptions.push(api.ui.registerToolbarAction('standup-message', { + label: 'Standup', + run: () => api.messages.send('Standup: yesterday, today, blocked') + })); +} +``` + +## Settings Page Plugin + +```json +{ + "schemaVersion": 1, + "id": "example.settings-page", + "title": "Settings Page Example", + "description": "Adds a plugin settings page and stores a local preference.", + "version": "1.0.0", + "kind": "client", + "apiVersion": "1.0.0", + "compatibility": { "minimumTojuVersion": "1.0.0" }, + "entrypoint": "./main.js", + "capabilities": ["ui.settings", "storage.local"], + "settings": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": true } + } + } +} +``` + +```js +export function activate(context) { + const { api } = context; + + context.subscriptions.push(api.ui.registerSettingsPage('preferences', { + label: 'Example Preferences', + render: () => { + const root = document.createElement('section'); + const button = document.createElement('button'); + + button.type = 'button'; + button.textContent = 'Remember preference'; + button.onclick = () => api.storage.set('enabled', true); + root.append(button); + return root; + } + })); +} +``` + +## Server-Scoped Soundboard + +A server-scoped plugin can be installed as a server requirement and auto-installed for server members when marked required. + +```json +{ + "schemaVersion": 1, + "id": "example.soundboard", + "title": "Server Soundboard", + "description": "Adds a soundboard side panel and announces played sounds.", + "version": "1.0.0", + "kind": "client", + "scope": "server", + "apiVersion": "1.0.0", + "compatibility": { "minimumTojuVersion": "1.0.0" }, + "entrypoint": "./main.js", + "capabilities": [ + "server.read", + "users.manage", + "ui.sidePanel", + "media.playAudio", + "messages.send" + ], + "pluginUser": { + "displayName": "Soundboard", + "label": "Audio helper" + } +} +``` + +```js +export function activate(context) { + const { api } = context; + const botId = api.server.registerPluginUser({ + id: 'soundboard-bot', + displayName: 'Soundboard' + }); + + context.subscriptions.push(api.ui.registerSidePanel('sounds', { + label: 'Soundboard', + render: () => { + const panel = document.createElement('div'); + const button = document.createElement('button'); + + button.type = 'button'; + button.textContent = 'Play chime'; + button.onclick = async () => { + await api.media.playAudioClip({ url: './chime.wav', volume: 0.7 }); + api.messages.sendAsPluginUser({ pluginUserId: botId, content: 'Played chime' }); + }; + + panel.append(button); + return panel; + } + })); +} +``` + +## Message Bus Plugin + +```json +{ + "schemaVersion": 1, + "id": "example.poll-bus", + "title": "Poll Bus", + "description": "Uses the plugin message bus for lightweight P2P poll votes.", + "version": "1.0.0", + "kind": "client", + "apiVersion": "1.0.0", + "compatibility": { "minimumTojuVersion": "1.0.0" }, + "entrypoint": "./main.js", + "capabilities": ["events.p2p.publish", "events.p2p.subscribe", "messages.read"] +} +``` + +```js +export function activate(context) { + const { api } = context; + + context.subscriptions.push(api.messageBus.subscribe({ + topic: 'poll:votes', + replayLatest: true, + latestMessageLimit: 20, + handler: (event) => api.logger.info('Vote received', event.payload) + })); + + api.messageBus.publish({ + topic: 'poll:votes', + payload: { option: 'A' }, + includeLatestMessages: true, + includeSelf: true, + latestMessageLimit: 20 + }); +} +``` + +## Custom DOM Mount + +Use `ui.dom` sparingly and cleanly. The runtime tags mounted elements with plugin ownership metadata and removes remaining mounted elements when the plugin unloads. + +```js +export function activate(context) { + const badge = document.createElement('div'); + + badge.textContent = 'Plugin active'; + badge.style.position = 'absolute'; + badge.style.right = '1rem'; + badge.style.bottom = '1rem'; + + context.subscriptions.push(context.api.ui.mountElement('active-badge', { + target: 'body', + element: badge + })); +} +``` + +## All-API Fixture + +The repo includes an E2E fixture at `toju-app/public/plugins/e2e-all-api/`. It intentionally calls every public plugin API surface so Playwright coverage can validate the runtime. Use it as a compatibility reference, not as the minimal style for production plugins. diff --git a/docs-site/docs/plugin-development/manifest.md b/docs-site/docs/plugin-development/manifest.md new file mode 100644 index 0000000..8686c62 --- /dev/null +++ b/docs-site/docs/plugin-development/manifest.md @@ -0,0 +1,164 @@ +--- +sidebar_position: 2 +--- + +# Manifest Model + +The manifest is the source of truth for plugin identity, compatibility, runtime shape, capabilities, data, events, UI hints, and distribution metadata. + +```ts +type TojuPluginInstallScope = 'client' | 'server'; +type PluginEventDirection = 'clientToServer' | 'serverRelay' | 'p2pHint'; +type PluginEventScope = 'server' | 'channel' | 'user' | 'plugin'; + +type PluginCapabilityId = + | 'profile.read' + | 'profile.write' + | 'users.read' + | 'users.manage' + | 'roles.read' + | 'roles.manage' + | 'messages.read' + | 'messages.send' + | 'messages.editOwn' + | 'messages.deleteOwn' + | 'messages.moderate' + | 'messages.sync' + | 'channels.read' + | 'channels.manage' + | 'server.read' + | 'server.manage' + | 'p2p.data' + | 'p2p.media' + | 'media.playAudio' + | 'media.addAudioStream' + | 'media.addVideoStream' + | 'audio.volume' + | 'audio.effects' + | 'ui.settings' + | 'ui.pages' + | 'ui.sidePanel' + | 'ui.channelsSection' + | 'ui.embeds' + | 'ui.dom' + | 'storage.local' + | 'storage.serverData.read' + | 'storage.serverData.write' + | 'events.server.publish' + | 'events.server.subscribe' + | 'events.p2p.publish' + | 'events.p2p.subscribe'; + +interface TojuPluginManifest { + schemaVersion: 1; + id: string; + title: string; + description: string; + version: string; + kind: 'client' | 'library'; + scope?: TojuPluginInstallScope; + apiVersion: string; + compatibility: { + minimumTojuVersion: string; + maximumTojuVersion?: string; + verifiedTojuVersion?: string; + }; + entrypoint?: string; + bundle?: { + url: string; + entrypoint?: string; + }; + readme?: string; + homepage?: string; + bugs?: string; + changelog?: string; + license?: string; + authors?: { + name: string; + email?: string; + url?: string; + }[]; + capabilities?: PluginCapabilityId[]; + events?: { + eventName: string; + direction: PluginEventDirection; + scope: PluginEventScope; + maxPayloadBytes?: number; + schema?: string; + }[]; + data?: { + key: string; + schema?: string; + scope: string; + storage: 'local' | 'serverData'; + }[]; + relationships?: { + after?: string[]; + before?: string[]; + conflicts?: string[]; + optional?: { id: string; versionRange?: string }[]; + requires?: { id: string; versionRange?: string }[]; + }; + load?: { + priority?: 'bootstrap' | 'high' | 'default' | 'low'; + }; + pluginUser?: { + avatar?: string; + displayName: string; + label?: string; + }; + settings?: Record; + ui?: Record; +} +``` + +## Required Fields + +| Field | Meaning | +| --- | --- | +| `schemaVersion` | Manifest schema version. Currently `1`. | +| `id` | Stable plugin id. Use a reverse-DNS or package-style id. | +| `title` | Human-readable plugin name. | +| `description` | Short explanation shown in plugin UI. | +| `version` | Plugin version. | +| `kind` | `client` for runtime plugins, `library` for shared dependency-style entries. | +| `apiVersion` | Plugin API version expected by the plugin. | +| `compatibility.minimumTojuVersion` | Oldest app version the plugin supports. | + +## Scope + +`scope: "client"` installs the plugin for the current client. Omit `scope` for the same behavior. + +`scope: "server"` marks a plugin as server-scoped. Server-scoped store entries can be installed to a chat server as requirements. Required server plugins are auto-installed for members when that server opens; optional requirements stay listed but do not auto-install. + +When a user installs a server-scoped plugin into the server they are currently viewing, MetoYou enables that plugin id locally and activates the plugin immediately after the local manifest is registered. Installing a server-scoped plugin for another server records the activation preference so it activates when that server is opened. + +## Entrypoint and Bundle + +Use `entrypoint` for a browser-resolvable module relative to the manifest. Use `bundle.url` when publishing a cached browser bundle through a plugin source manifest. Desktop installs cache bundle files into app data and load the cached manifest afterward. + +## Events + +Every server or P2P plugin event should be declared before it is published or subscribed to. + +```json +{ + "events": [ + { + "eventName": "poll:vote", + "direction": "p2pHint", + "scope": "channel", + "maxPayloadBytes": 2048 + } + ] +} +``` + +## Data Declarations + +Use `data` to document plugin-owned data keys and intended storage. + +- `local` maps to client-local plugin data. +- `serverData` maps to local per-user/per-server plugin data. + +Signal server HTTP persistence for arbitrary plugin data is disabled by design. diff --git a/docs-site/docs/user-guide/first-steps.md b/docs-site/docs/user-guide/first-steps.md new file mode 100644 index 0000000..519e861 --- /dev/null +++ b/docs-site/docs/user-guide/first-steps.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 1 +--- + +# First Steps + +MetoYou is a chat app for servers, text conversations, direct messages, and live voice. You do not need to understand the technical parts to use it. + +## Main Words + +| Word | Meaning | +| --- | --- | +| Server | A shared space for a community, team, or group. | +| Text channel | A named chat room inside a server. Messages stay in that channel. | +| Voice channel | A named live room inside a server. Join it when you want to talk, share camera, or share screen. | +| Direct message | A private conversation outside a server channel. | +| Plugin | An add-on that can add buttons, panels, tools, integrations, or server-specific features. | + +## Sign In + +1. Open MetoYou. +2. Sign in with your username and password. +3. If you use more than one signaling server, choose the server endpoint that owns your account. + +A signaling server handles accounts, server discovery, membership, and connection setup. In normal use you can think of it as the place MetoYou checks when you log in and join servers. + +## Find a Server + +1. Open the server search page. +2. Search by server name or browse the available list. +3. Select a server. +4. Join directly if it is public, enter the password if it is protected, or use an invite link if someone sent you one. + +After joining, the server appears in the vertical server rail on the left. Click a server icon there to switch servers. + +## Read and Send Messages + +1. Click a server in the left rail. +2. Pick a text channel under **Text Channels**. +3. Type in the composer at the bottom of the chat. +4. Press Enter or use the send button. + +Text channels keep different topics separate. For example, a server might have `general`, `announcements`, and `support` as separate text channels. + +## Start Talking + +1. Open a server. +2. Pick a voice channel under **Voice Channels**. +3. Click the voice channel to join. +4. Use the voice controls to mute, deafen, start camera, share screen, or leave. + +Voice is live. Text messages are written chat. They can happen at the same time, but they are different channel types. + +## Use Direct Messages + +Direct messages are one-to-one conversations. They are separate from server text channels, so they do not depend on which server you are viewing. + +## Open Settings + +Settings contain account, voice, plugin, server, desktop, update, local API, theme, and data controls. Desktop users can also manage local data import/export and local documentation/API hosting. + +## Install Plugins + +Plugins are installed from the Plugin Store or Plugin Manager. Some plugins are global client plugins. Other plugins are server-scoped and only apply to a specific server. + +See [Plugins for Users](./plugins.md) for the full non-technical plugin guide. \ No newline at end of file diff --git a/docs-site/docs/user-guide/plugins.md b/docs-site/docs/user-guide/plugins.md new file mode 100644 index 0000000..21b2f2a --- /dev/null +++ b/docs-site/docs/user-guide/plugins.md @@ -0,0 +1,82 @@ +--- +sidebar_position: 5 +--- + +# Plugins for Users + +Plugins add features to MetoYou. They can add pages, buttons, panels, settings, sounds, message tools, custom embeds, or server-specific behavior. + +## Types of Plugins + +| Type | What it means | +| --- | --- | +| Client plugin | Installed for your app. It follows you across servers when active. | +| Server plugin | Installed for a specific server. It may be required, recommended, optional, blocked, or incompatible. | +| Library plugin | Shared plugin code used by other plugins. It is not normally something users interact with directly. | + +## Install from the Plugin Store + +1. Open the Plugin Store from the title bar or Settings. +2. Browse or search available plugins. +3. Open the plugin details. +4. Read the description, version, source, and capability list. +5. Choose install. +6. Review and grant only the capabilities you trust. +7. Activate the plugin. + +Server-scoped plugins installed to the server you are currently viewing are enabled and activated automatically after install, so their panels, actions, or embeds can appear immediately. + +## Install a Local Plugin + +Desktop builds can discover local plugin folders from the app data plugins directory. + +1. Put the plugin folder in the desktop plugins directory. +2. Open Settings. +3. Open the Plugin Manager. +4. Refresh or register local plugins. +5. Grant capabilities and activate the plugin. + +## Server Plugin Prompts + +When a server uses plugins, MetoYou may show a prompt. + +| Status | Meaning | +| --- | --- | +| Required | You must install the plugin to join or continue using that server. | +| Recommended | The server suggests the plugin, but you can choose. | +| Optional | The plugin is available for the server, but not required. | +| Blocked | The server marks the plugin as not allowed. | +| Incompatible | The plugin version does not work with your app version or the server requirement. | + +Required plugins are still installed locally on your device. The signaling server stores requirement metadata only; it does not run plugin code. + +## Capability Grants + +Plugins must ask for capabilities before using sensitive features. + +Examples: + +| Capability area | Why a plugin might ask | +| --- | --- | +| Messages | Send messages, read current messages, moderate messages, or render embeds. | +| Users and roles | Read member lists, create plugin users, or manage users. | +| Voice and media | Play audio, add an audio stream, add a video stream, or adjust volume. | +| UI | Add pages, settings pages, side panels, toolbar buttons, or DOM elements. | +| Storage | Save plugin preferences locally or per server. | + +Only grant capabilities to plugins you trust. + +## Manage Plugins + +The Plugin Manager lets you: + +- activate, deactivate, reload, or unload plugins; +- grant or revoke capabilities; +- inspect plugin logs; +- see plugin UI contribution counts; +- review server plugin requirements; +- uninstall plugins. + +## Plugin Safety Notes + +Plugins are browser-safe JavaScript modules loaded by the client. They do not run on the signaling server. A plugin can only call privileged MetoYou APIs when its manifest declares the capability and you grant it. \ No newline at end of file diff --git a/docs-site/docs/user-guide/servers-and-channels.md b/docs-site/docs/user-guide/servers-and-channels.md new file mode 100644 index 0000000..0ba24b3 --- /dev/null +++ b/docs-site/docs/user-guide/servers-and-channels.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 2 +--- + +# Servers and Channels + +A server is the main shared space in MetoYou. Servers contain members, channels, permissions, optional plugins, and server settings. + +## Server Rail + +The server rail is the vertical list of servers on the left side of the app. + +- Click a server icon to open it. +- Use the add/search control to find or join more servers. +- A badge can show unread activity. +- Server context actions can include invite, leave, or server settings depending on your permissions. + +## Text Channels + +Text channels are written conversations. Each text channel has its own message list. + +Common examples: + +| Channel | Use | +| --- | --- | +| `general` | Everyday chat. | +| `announcements` | Updates from owners or admins. | +| `support` | Help requests. | +| `clips` | Shared media or links. | + +Messages, replies, reactions, attachments, GIFs, typing indicators, and plugin-created messages are scoped to the active text channel. + +## Voice Channels + +Voice channels are live spaces. Joining a voice channel connects your microphone and lets you use camera or screen sharing when enabled. + +Voice channel examples: + +| Channel | Use | +| --- | --- | +| `Lobby` | Casual drop-in voice. | +| `Gaming` | In-game voice. | +| `Meeting` | Focused calls. | +| `Support Room` | Live help. | + +## Text Channels vs Voice Channels + +| Text channel | Voice channel | +| --- | --- | +| Written messages. | Live audio and media. | +| You can read later. | You join and leave in real time. | +| Uses the message composer. | Uses voice controls. | +| Good for searchable discussions. | Good for conversations, calls, screen shares, and quick coordination. | + +## Server Members + +The member list shows people known to the server. Online members appear separately from offline members. Depending on permissions, owners, admins, or moderators can move users between voice channels, kick users, ban users, or change roles. + +## Invites + +Invite links help other users join a server. If a server is private or password-protected, the invite or password controls who can enter. + +## Server Plugins + +A server can recommend or require plugins. Required server plugins may block joining until you choose whether to install them. Optional and recommended plugins can be skipped. \ No newline at end of file diff --git a/docs-site/docs/user-guide/settings.md b/docs-site/docs/user-guide/settings.md new file mode 100644 index 0000000..70e21a9 --- /dev/null +++ b/docs-site/docs/user-guide/settings.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 6 +--- + +# Settings and Data + +Settings control the app, voice, plugins, servers, themes, updates, local APIs, and desktop behavior. + +## Common Settings + +| Area | What you can manage | +| --- | --- | +| Account | Current profile, display details, and avatar metadata. | +| Voice | Devices, volumes, bitrate, latency, noise reduction, screen share preferences. | +| Plugins | Installed plugins, capability grants, plugin logs, and plugin store sources. | +| Server | Server details, channels, roles, moderation, plugin requirements, and member controls. | +| Theme | App colors and visual preferences. | +| Desktop | Tray behavior, auto-start, hardware acceleration, updates, and local data tools. | +| Local API | Local HTTP server, API docs, Docusaurus docs, and allowed signaling servers. | + +## Local Data + +Desktop MetoYou stores local app data on your device. That can include rooms, messages, users, plugin data, settings, and metadata. The desktop settings include data import/export tools. + +## Local API and Documentation Hosting + +The desktop app can start a local HTTP server. It is off by default. When enabled, it can serve: + +- Local REST API endpoints under `/api/...`; +- Scalar REST API docs at `/docs`; +- this Docusaurus site at `/docusaurus`. + +Authentication for protected local API routes uses a local bearer token. Login is checked against an allowed signaling server that you configure in settings. \ No newline at end of file diff --git a/docs-site/docs/user-guide/text-and-direct-messages.md b/docs-site/docs/user-guide/text-and-direct-messages.md new file mode 100644 index 0000000..06c2054 --- /dev/null +++ b/docs-site/docs/user-guide/text-and-direct-messages.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 3 +--- + +# Text and Direct Messages + +Text channels and direct messages both use written chat, but they are meant for different situations. + +## Text Channels + +Text channels belong to a server. Everyone with access to that server and channel can participate. + +You can use text channels to: + +- send normal messages; +- edit or delete your own messages when allowed; +- react to messages; +- send attachments; +- browse and send GIFs when available; +- see typing indicators; +- read synced message history stored on your device. + +## Direct Messages + +Direct messages are private conversations outside a server channel. Use them when a message is meant for one person instead of the server. + +## Attachments and Media + +Attachments can appear as files, images, audio, or video depending on the file type and what the app can preview. If an image or link cannot load directly, the app can use fallback paths where available. + +## Message Sync + +MetoYou stores messages locally and syncs recent messages with peers when connections are available. If you were offline, messages may appear after peers reconnect and exchange their recent message lists. + +## Plugin Messages + +Some plugins can send messages, create bot-style plugin users, render custom embeds, or add composer buttons. MetoYou asks for plugin capability grants before plugins can use privileged message features. \ No newline at end of file diff --git a/docs-site/docs/user-guide/voice-channels.md b/docs-site/docs/user-guide/voice-channels.md new file mode 100644 index 0000000..845b012 --- /dev/null +++ b/docs-site/docs/user-guide/voice-channels.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 4 +--- + +# Voice Channels and Calls + +Voice channels are live rooms inside a server. Join one when you want to talk, share camera, or share your screen. + +## Join a Voice Channel + +1. Open the server from the left server rail. +2. Find **Voice Channels** in the server side panel. +3. Click the voice channel you want to join. +4. Allow microphone access if your system asks. +5. Use the voice controls to manage your call. + +When you join, other users in the same voice channel can hear you unless you are muted. Users in other voice channels are not part of your live voice room. + +## Voice Controls + +The voice controls can include: + +| Control | What it does | +| --- | --- | +| Mute microphone | Stops sending your microphone audio. | +| Deafen | Stops playback and usually mutes your microphone too. | +| Camera | Starts or stops webcam video. | +| Screen share | Shares a screen or window. | +| Settings | Opens voice device and quality settings. | +| Leave | Disconnects from the voice channel. | + +## Screen Sharing + +1. Join a voice channel. +2. Click screen share. +3. Choose a screen or window. +4. Choose whether to include system audio when available. +5. Stop sharing from the voice controls when done. + +The screen share picker can show screens and windows. Desktop audio support depends on operating system support and the selected source. + +## Voice Workspace + +When someone shares camera or screen, the voice workspace can expand into a larger media area. It can show focused streams, a grid of streams, or a minimized mini-window. + +## Floating Voice Controls + +If you navigate away from the server while still connected to voice, MetoYou can show floating voice controls. Use them to return to the voice server or leave the call. + +## Voice Settings + +Voice settings can include: + +- input device; +- output device; +- input volume; +- output volume; +- audio bitrate; +- latency profile; +- noise reduction; +- screen share quality; +- system audio preference. + +## Troubleshooting + +| Problem | Try this | +| --- | --- | +| Nobody hears you | Check mute, input device, system microphone permission, and input volume. | +| You hear nobody | Check deafen, output device, output volume, and whether others are in the same voice channel. | +| Screen share is missing | Check desktop permissions and try a different screen or window. | +| Voice drops after switching servers | Return to the server with the active voice session or leave and rejoin the voice channel. | + +Voice and screen sharing use peer-to-peer WebRTC media. The signaling server helps users connect, but the media itself travels through peer connections. \ No newline at end of file diff --git a/docs-site/docs/using-metoyou.md b/docs-site/docs/using-metoyou.md new file mode 100644 index 0000000..a359391 --- /dev/null +++ b/docs-site/docs/using-metoyou.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 2 +--- + +# Using MetoYou + +## Sign In + +MetoYou signs in through a signaling server. The signaling server validates the user account, coordinates server membership, relays selected realtime messages, and helps peers establish WebRTC connections. + +For the desktop Local API, the same signaling server allow-list is used before local bearer tokens can be issued. This keeps local automation tied to servers you explicitly trust. + +## Find or Join Servers + +Use the server search flow to find known servers. A server can be public, private, or password-protected depending on its settings. Invite links can be created from the title bar menu while a server is active. + +A server contains: + +- basic profile information such as name, topic, description, privacy, and maximum users; +- text channels; +- voice or custom channel sections; +- roles and permissions; +- members and voice state; +- optional server-scoped plugin requirements. + +## Text Channels and Messages + +Text channels are selected inside the active server. Messages are persisted locally by the client and synchronized through realtime events while connected. Plugins with the relevant capabilities can read, send, edit, delete, moderate, or sync messages. + +Direct messages use the same shell but are not part of a room channel context. + +## Voice, Video, and Screen Sharing + +Voice and media are peer-to-peer. The signaling server coordinates connection setup, while media streams travel through WebRTC peer connections. + +Desktop builds include platform integrations such as Linux display-server detection and optional monitor audio routing for screen sharing. Plugin media APIs can contribute custom audio or video streams when the user grants the necessary capabilities. + +## Plugins + +Open the Plugin Store from the title bar package button or menu. The plugin manager separates global client plugins from server-scoped plugins. Installed plugins can be activated, reloaded, unloaded, disabled, inspected for logs, and granted capabilities. + +Plugins are explicit runtime modules. MetoYou loads browser-safe ES modules, passes a frozen API object, and cleans up registered disposables when a plugin unloads. + +## Desktop Settings + +Desktop settings cover: + +- auto-start and close-to-tray behavior; +- hardware acceleration and Linux VA-API video encode options; +- update manifests and target update versions; +- local HTTP API hosting; +- Scalar API documentation; +- Docusaurus app/plugin documentation; +- allowed signaling servers for local API authentication; +- local plugin discovery and store sources; +- themes and user data import/export. diff --git a/docs-site/docusaurus.config.ts b/docs-site/docusaurus.config.ts new file mode 100644 index 0000000..386629d --- /dev/null +++ b/docs-site/docusaurus.config.ts @@ -0,0 +1,71 @@ +import type { Config } from '@docusaurus/types'; +import type * as Preset from '@docusaurus/preset-classic'; + +const config: Config = { + title: 'MetoYou Docs', + tagline: 'Desktop chat, local APIs, and plugin development', + url: 'http://127.0.0.1', + baseUrl: '/docusaurus/', + organizationName: 'metoyou', + projectName: 'metoyou', + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', + i18n: { + defaultLocale: 'en', + locales: ['en'] + }, + presets: [ + [ + 'classic', + { + docs: { + routeBasePath: '/', + sidebarPath: './sidebars.ts' + }, + blog: false, + theme: { + customCss: './src/css/custom.css' + } + } satisfies Preset.Options + ] + ], + themeConfig: { + navbar: { + title: 'MetoYou Docs', + items: [ + { type: 'docSidebar', sidebarId: 'mainSidebar', position: 'left', label: 'Guides' }, + { to: '/user-guide/first-steps', label: 'User Guide', position: 'left' }, + { to: '/developer/contributing', label: 'Developer Guide', position: 'left' }, + { to: '/plugin-development/create-a-plugin', label: 'Plugin Guide', position: 'left' }, + { to: '/developer/rest-api', label: 'REST API', position: 'left' } + ] + }, + footer: { + style: 'dark', + links: [ + { + title: 'Docs', + items: [ + { label: 'First Steps', to: '/user-guide/first-steps' }, + { label: 'Voice Channels', to: '/user-guide/voice-channels' }, + { label: 'Plugins for Users', to: '/user-guide/plugins' }, + { label: 'Contributing', to: '/developer/contributing' }, + { label: 'Create a Plugin', to: '/plugin-development/create-a-plugin' }, + { label: 'Plugin API Reference', to: '/plugin-development/api-reference' }, + { label: 'Local REST API', to: '/developer/rest-api' } + ] + } + ], + copyright: 'MetoYou local documentation. Built with Docusaurus.' + }, + prism: { + additionalLanguages: [ + 'bash', + 'json', + 'typescript' + ] + } + } satisfies Preset.ThemeConfig +}; + +export default config; diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json new file mode 100644 index 0000000..6f0d272 --- /dev/null +++ b/docs-site/package-lock.json @@ -0,0 +1,18490 @@ +{ + "name": "metoyou-docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "metoyou-docs", + "version": "1.0.0", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/preset-classic": "3.10.0", + "@mdx-js/react": "^3.1.1", + "clsx": "^2.1.1", + "prism-react-renderer": "^2.4.1", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.10.0", + "@docusaurus/tsconfig": "3.10.0", + "@docusaurus/types": "3.10.0", + "typescript": "~5.9.2" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.17.0.tgz", + "integrity": "sha512-nuhHZdTiCtRzJEe9VSNzyqE9cOQMt01UWBzymFnjbgwrxxZpbGHQde6Oa/y9zyspTCjbUtb7Q5HQek1CLiLyeg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.8.tgz", + "integrity": "sha512-3YEorYg44niXcm7gkft3nXYItHd44e8tmh4D33CTszPgP0QWkaLEaFywiNyJBo7UL/mqObA/G9RYuU7R8tN1IA==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.8", + "@algolia/autocomplete-shared": "1.19.8" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.8.tgz", + "integrity": "sha512-ZvJWO8ZZJDpc1LNM2TTBdmQsZBLMR4rU5iNR2OYvEeFBiaf/0ESnRSSLQbryarJY4SVxtoz6A2ZtDMNM+iQEAA==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.8" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.8.tgz", + "integrity": "sha512-h5hf2t8ejF6vlOgvLaZzQbWs5SyH2z4PAWygNAvvD/2RI29hdQ54ldUGwqVuj9Srs+n8XUKTPUqb7fvhBhQrnQ==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.51.0.tgz", + "integrity": "sha512-PKrKlIla1U2J7mFcIQn6N3pWP4oySmkxShnbbDsj/H7818gKbET5KsUwsVoNjWIxHKTJMCTcQ7ekAJ8Ea23NMg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.51.0.tgz", + "integrity": "sha512-U+HCY1K16Km91pIRL1kN6bW6BbGFAF/WhkRSCx4wyl1aFpbrlhSFQs/dAwWbmyBiHWwVWhl7stWHQ1pum5EfMw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.51.0.tgz", + "integrity": "sha512-YPJ3dEuZLCRp846Az94t6Z2gwSNRazP+SmBco7p6SCa4fYrtIE820PDXYZshbNrj2Z8Qfbmv7BQ1Lecl5L3G/w==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.51.0.tgz", + "integrity": "sha512-/gEwLlR7fQ7YjOW+ADRZ0NxLDtpTC61FSzlZ01Gdl1kTJfU0Rq3Y/TYqwxGxlQGcUiXtGzrpjxXWh3Y0TZD6NA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.51.0.tgz", + "integrity": "sha512-nRwUN1Y2cKyOAFZyIBagkEfZSIhP05nWhT4Rjwl84lcjECssYggftrAODrZ4leakXxSGjhxs/AdaAFEIBqwVFA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.51.0.tgz", + "integrity": "sha512-pybzYCG7VoQKppo+z5iZOKpW8XqtFxhsAIRgEaNboCnfypKukiBHJAwB+pmr7vMZXBsOHwklGYWwCG82e8qshA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.51.0.tgz", + "integrity": "sha512-DWVIlj6RqcvdhwP0gBU9OpOQPnHdcAk9jlT+z8rsNb2+liWv4eUlfQZ7saGBraFsnygEHD3PtdppIHvqwBAb5w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "license": "MIT" + }, + "node_modules/@algolia/ingestion": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.51.0.tgz", + "integrity": "sha512-bA25s12iUDJi/X8M7tWlPRT8GeOhls/yDbdoUqinz27lNqsOlcM1UrAxIKdIZ6lm3sXit+ewPIz1pS2x6rXu8g==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.51.0.tgz", + "integrity": "sha512-zj+RcE5e0NE0/ew6oEOTgOplPHry+w2oi7h0Y87lhdq4E0d7xLS31KVB8kKfCGkrG7AYtZvrcyvLOKS5d0no4Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.51.0.tgz", + "integrity": "sha512-/HDgccfye1Rq3bPxaSCsvSEBHzSMmtpM9ZRGRtAuC62Cv+ql/76IWnxjGTDXtqIJ+/j7ZlFYAzq9fkp95wF2SQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.51.0.tgz", + "integrity": "sha512-nJdW+WBwGlXWMJbxxB7/AJPvNq0lLJSudXmIQCJbmH8jsOXQhRpAtoCD4ceLyJKv3ze9JbQu4Gqu5JDLckuFcw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.51.0.tgz", + "integrity": "sha512-bsBgRI/1h1mjS3eCyfGau78yGZVmiDLmT1aU6dMnk75/T0SgKqnSKNpQ53xKoDYVChGDcNnpHXWpoUSo8MH1+w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.51.0.tgz", + "integrity": "sha512-zPrIDVPpmKWgrjmWOqpqrhqAhNjvVkjoj+mqw2NBPxEOuKNzP0H+Qz5NJLLTOepBVj1UFedFaF3AUgxLsB9ukQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", + "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", + "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/postcss-alpha-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", + "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", + "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", + "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", + "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", + "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", + "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", + "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", + "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", + "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", + "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", + "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.1.tgz", + "integrity": "sha512-TQUGBuRvxdc7TgNSTevYqrL8oItxiwPDixk20qCB5me/W8uF7BPbhRrAvFuhEoywQp/woRsUZ6SJ+sU5idZAIA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", + "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-position-area-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-position-area-property/-/postcss-position-area-property-1.0.0.tgz", + "integrity": "sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", + "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-property-rule-prelude-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-property-rule-prelude-list/-/postcss-property-rule-prelude-list-1.0.0.tgz", + "integrity": "sha512-IxuQjUXq19fobgmSSvUDO7fVwijDJaZMvWQugxfEUxmjBeDCVaDuMpsZ31MsTm5xbnhA+ElDi0+rQ7sQQGisFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", + "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-syntax-descriptor-syntax-production": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-syntax-descriptor-syntax-production/-/postcss-syntax-descriptor-syntax-production-1.0.1.tgz", + "integrity": "sha512-GneqQWefjM//f4hJ/Kbox0C6f2T7+pi4/fqTqOFGTL3EjnvOReTqO1qUQ30CaUjkwjYq9qZ41hzarrAxCc4gow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-system-ui-font-family": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-system-ui-font-family/-/postcss-system-ui-font-family-1.0.0.tgz", + "integrity": "sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", + "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/core": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.6.3.tgz", + "integrity": "sha512-rUOujwIpxJRgD7+kicVsI3D5sqBvdiRTquzWBpTEXZs8ZXfGbfzpus5HqumaNYTppN2HvH8E2yNuRwYdHJeOlA==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@docsearch/css": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.3.tgz", + "integrity": "sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ==", + "license": "MIT" + }, + "node_modules/@docsearch/react": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.6.3.tgz", + "integrity": "sha512-Bg2wdDsoQVlNCcEKuEJAU04tvHCqgx8rIu+uIoM4pRtcx3TBKJuXutJik3LTA8LRc9YEyHkrYUrmcC0D7BYf+g==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.6.3", + "@docsearch/css": "4.6.3" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@docusaurus/babel": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.10.0.tgz", + "integrity": "sha512-mqCJhCZNZUDg0zgDEaPTM4DnRsisa24HdqTy/qn/MQlbwhTb4WVaZg6ZyX6yIVKqTz8fS1hBMgM+98z+BeJJDg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/preset-typescript": "^7.25.9", + "@babel/runtime": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@docusaurus/logger": "3.10.0", + "@docusaurus/utils": "3.10.0", + "babel-plugin-dynamic-import-node": "^2.3.3", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/bundler": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.10.0.tgz", + "integrity": "sha512-iONUGZGgp+lAkw/cJZH6irONcF4p8+278IsdRlq8lYhxGjkoNUs0w7F4gVXBYSNChq5KG5/JleTSsdJySShxow==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@docusaurus/babel": "3.10.0", + "@docusaurus/cssnano-preset": "3.10.0", + "@docusaurus/logger": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "babel-loader": "^9.2.1", + "clean-css": "^5.3.3", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.11.0", + "css-minimizer-webpack-plugin": "^5.0.1", + "cssnano": "^6.1.2", + "file-loader": "^6.2.0", + "html-minifier-terser": "^7.2.0", + "mini-css-extract-plugin": "^2.9.2", + "null-loader": "^4.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", + "terser-webpack-plugin": "^5.3.9", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "webpack": "^5.95.0", + "webpackbar": "^6.0.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/core": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.10.0.tgz", + "integrity": "sha512-mgLdQsO8xppnQZc3LPi+Mf+PkPeyxJeIx11AXAq/14fsaMefInQiMEZUUmrc7J+956G/f7MwE7tn8KZgi3iRcA==", + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.10.0", + "@docusaurus/bundler": "3.10.0", + "@docusaurus/logger": "3.10.0", + "@docusaurus/mdx-loader": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-common": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "boxen": "^6.2.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cli-table3": "^0.6.3", + "combine-promises": "^1.1.0", + "commander": "^5.1.0", + "core-js": "^3.31.1", + "detect-port": "^1.5.1", + "escape-html": "^1.0.3", + "eta": "^2.2.0", + "eval": "^0.1.8", + "execa": "^5.1.1", + "fs-extra": "^11.1.1", + "html-tags": "^3.3.1", + "html-webpack-plugin": "^5.6.0", + "leven": "^3.1.0", + "lodash": "^4.17.21", + "open": "^8.4.0", + "p-map": "^4.0.0", + "prompts": "^2.4.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", + "react-loadable-ssr-addon-v5-slorber": "^1.0.3", + "react-router": "^5.3.4", + "react-router-config": "^5.1.1", + "react-router-dom": "^5.3.4", + "semver": "^7.5.4", + "serve-handler": "^6.1.7", + "tinypool": "^1.0.2", + "tslib": "^2.6.0", + "update-notifier": "^6.0.2", + "webpack": "^5.95.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-dev-server": "^5.2.2", + "webpack-merge": "^6.0.1" + }, + "bin": { + "docusaurus": "bin/docusaurus.mjs" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*", + "@mdx-js/react": "^3.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/cssnano-preset": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.0.tgz", + "integrity": "sha512-qzSshTO1DB3TYW+dPUal5KHM7XPc5YQfzF3Kdb2NDACJUyGbNcFtw3tGkCJlYwhNCRKbZcmwraKUS1i5dcHdGg==", + "license": "MIT", + "dependencies": { + "cssnano-preset-advanced": "^6.1.2", + "postcss": "^8.5.4", + "postcss-sort-media-queries": "^5.2.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/logger": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.10.0.tgz", + "integrity": "sha512-9jrZzFuBH1LDRlZ7cznAhCLmAZ3HSDqgwdrSSZdGHq9SPUOQgXXu8mnxe2ZRB9NS1PCpMTIOVUqDtZPIhMafZg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/mdx-loader": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.10.0.tgz", + "integrity": "sha512-mQQV97080AH4PYNs087l202NMDqRopZA4mg5W76ZZyTFrmWhJ3mHg+8A+drJVENxw5/Q+wHMHLgsx+9z1nEs0A==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "@mdx-js/mdx": "^3.0.0", + "@slorber/remark-comment": "^1.0.0", + "escape-html": "^1.0.3", + "estree-util-value-to-estree": "^3.0.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "image-size": "^2.0.2", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-raw": "^7.0.0", + "remark-directive": "^3.0.0", + "remark-emoji": "^4.0.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "stringify-object": "^3.3.0", + "tslib": "^2.6.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0", + "url-loader": "^4.1.1", + "vfile": "^6.0.1", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/module-type-aliases": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.0.tgz", + "integrity": "sha512-/1O0Zg8w3DFrYX/I6Fbss7OJrtZw1QoyjDhegiFNHVi9A9Y0gQ3jUAytVxF6ywpAWpLyLxch8nN8H/V3XfzdJQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.10.0", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "@types/react-router-dom": "*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@docusaurus/plugin-content-blog": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.0.tgz", + "integrity": "sha512-RuTz68DhB7CL96QO5UsFbciD7GPYq6QV+YMfF9V0+N4ZgLhJIBgpVAr8GobrKF6NRe5cyWWETU5z5T834piG9g==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/logger": "3.10.0", + "@docusaurus/mdx-loader": "3.10.0", + "@docusaurus/theme-common": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-common": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "cheerio": "1.0.0-rc.12", + "combine-promises": "^1.1.0", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-docs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.0.tgz", + "integrity": "sha512-9BjHhf15ct8Z7TThTC0xRndKDVvMKmVsAGAN7W9FpNRzfMdScOGcXtLmcCWtJGvAezjOJIm6CxOYCy3Io5+RnQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/logger": "3.10.0", + "@docusaurus/mdx-loader": "3.10.0", + "@docusaurus/module-type-aliases": "3.10.0", + "@docusaurus/theme-common": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-common": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "@types/react-router-config": "^5.0.7", + "combine-promises": "^1.1.0", + "fs-extra": "^11.1.1", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "tslib": "^2.6.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-pages": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.0.tgz", + "integrity": "sha512-5amX8kEJI+nIGtuLVjYk59Y5utEJ3CHETFOPEE4cooIRLA4xM4iBsA6zFgu4ljcopeYwvBzFEWf5g2I6Yb9SkA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/mdx-loader": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.0.tgz", + "integrity": "sha512-6q1vtt5FJcg5osgkHeM1euErECNqEZ5Z1j69yiNx2luEBIso+nxCkS9nqj8w+MK5X7rvKEToGhFfOFWncs51pQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.10.0.tgz", + "integrity": "sha512-XcljKN+G+nmmK69uQA1d9BlYU3ZftG3T3zpK8/7Hf/wrOlV7TA4Ampdrdwkg0jElKdKAoSnPhCO0/U3bQGsVQQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "fs-extra": "^11.1.1", + "react-json-view-lite": "^2.3.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-analytics": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.0.tgz", + "integrity": "sha512-hTEoodatpBZnUat5nFExbuTGA1lhWGy7vZGuTew5Q3QDtGKFpSJLYmZJhdTjvCFwv1+qQ67hgAVlKdJOB8TXow==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-gtag": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.0.tgz", + "integrity": "sha512-iB/Zzjv/eelJRbdULZqzWCbgMgJ7ht4ONVjXtN3+BI/muil6S87gQ1OJyPwlXD+ELdKkitC7bWv5eJdYOZLhrQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "@types/gtag.js": "^0.0.20", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-tag-manager": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.0.tgz", + "integrity": "sha512-FEjZxqKgLHa+Wez/EgKxRwvArNCWIScfyEQD95rot7jkxp6nonjI5XIbGfO/iYhM5Qinwe8aIEQHP2KZtpqVuA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-sitemap": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.0.tgz", + "integrity": "sha512-DVTSLjB97hIjmayGnGcBfognCeI7ZuUKgEnU7Oz81JYqXtVg94mVTthDjq3QHTylYNeCUbkaW8VF0FDLcc8pPw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/logger": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-common": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "fs-extra": "^11.1.1", + "sitemap": "^7.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-svgr": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.0.tgz", + "integrity": "sha512-lNljBESaETZqVBMPqkrGchr+UPT1eZzEPLmJhz8I76BxbjqgsUnRvrq6lQJ9sYjgmgX52KB7kkgczqd2yzoswQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "@svgr/core": "8.1.0", + "@svgr/webpack": "^8.1.0", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/preset-classic": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.10.0.tgz", + "integrity": "sha512-kw/Ye02Hc6xP1OdTswy8yxQEHg0fdPpyWAQRxr5b2x3h7LlG2Zgbb5BDFROnXDDMpUxB7YejlocJIE5HIEfpNA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/plugin-content-blog": "3.10.0", + "@docusaurus/plugin-content-docs": "3.10.0", + "@docusaurus/plugin-content-pages": "3.10.0", + "@docusaurus/plugin-css-cascade-layers": "3.10.0", + "@docusaurus/plugin-debug": "3.10.0", + "@docusaurus/plugin-google-analytics": "3.10.0", + "@docusaurus/plugin-google-gtag": "3.10.0", + "@docusaurus/plugin-google-tag-manager": "3.10.0", + "@docusaurus/plugin-sitemap": "3.10.0", + "@docusaurus/plugin-svgr": "3.10.0", + "@docusaurus/theme-classic": "3.10.0", + "@docusaurus/theme-common": "3.10.0", + "@docusaurus/theme-search-algolia": "3.10.0", + "@docusaurus/types": "3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-classic": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.10.0.tgz", + "integrity": "sha512-9msCAsRdN+UG+RwPwCFb0uKy4tGoPh5YfBozXeGUtIeAgsMdn6f3G/oY861luZ3t8S2ET8S9Y/1GnpJAGWytww==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/logger": "3.10.0", + "@docusaurus/mdx-loader": "3.10.0", + "@docusaurus/module-type-aliases": "3.10.0", + "@docusaurus/plugin-content-blog": "3.10.0", + "@docusaurus/plugin-content-docs": "3.10.0", + "@docusaurus/plugin-content-pages": "3.10.0", + "@docusaurus/theme-common": "3.10.0", + "@docusaurus/theme-translations": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-common": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "copy-text-to-clipboard": "^3.2.0", + "infima": "0.2.0-alpha.45", + "lodash": "^4.17.21", + "nprogress": "^0.2.0", + "postcss": "^8.5.4", + "prism-react-renderer": "^2.3.0", + "prismjs": "^1.29.0", + "react-router-dom": "^5.3.4", + "rtlcss": "^4.1.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.0.tgz", + "integrity": "sha512-Dkp1YXKn16ByCJAdIjbDIOpVb4Z66MsVD694/ilX1vAAHaVEMrVsf/NPd9VgreyFx08rJ9GqV1MtzsbTcU73Kg==", + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "3.10.0", + "@docusaurus/module-type-aliases": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-common": "3.10.0", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "clsx": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "prism-react-renderer": "^2.3.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-search-algolia": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.0.tgz", + "integrity": "sha512-f5FPKI08e3JRG63vR/o4qeuUVHUHzFzM0nnF+AkB67soAZgNsKJRf2qmUZvlQkGwlV+QFkKe4D0ANMh1jToU3g==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "^1.19.2", + "@docsearch/react": "^3.9.0 || ^4.3.2", + "@docusaurus/core": "3.10.0", + "@docusaurus/logger": "3.10.0", + "@docusaurus/plugin-content-docs": "3.10.0", + "@docusaurus/theme-common": "3.10.0", + "@docusaurus/theme-translations": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-validation": "3.10.0", + "algoliasearch": "^5.37.0", + "algoliasearch-helper": "^3.26.0", + "clsx": "^2.0.0", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-translations": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.10.0.tgz", + "integrity": "sha512-L9IbFLwTc5+XdgH45iQYufLn0SVZd6BUNelDbKIFlH+E4hhjuj/XHWAFMX/w2K59rfy8wak9McOaei7BSUfRPA==", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/tsconfig": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.10.0.tgz", + "integrity": "sha512-TXdC3WXuPrdQAexLvjUJfnYf3YKEgEqAs5nK0Q88pRBCW7t7oN4ILvWYb3A5Z1wlSXyXGWW/mCUmLEhdWsjnDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docusaurus/types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.10.0.tgz", + "integrity": "sha512-F0dOt3FOoO20rRaFK7whGFQZ3ggyrWEdQc/c8/UiRuzhtg4y1w9FspXH5zpCT07uMnJKBPGh+qNazbNlCQqvSw==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "@types/history": "^4.7.11", + "@types/mdast": "^4.0.2", + "@types/react": "*", + "commander": "^5.1.0", + "joi": "^17.9.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.95.0", + "webpack-merge": "^5.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/types/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docusaurus/utils": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.0.tgz", + "integrity": "sha512-T3B0WTigsIthe0D4LQa2k+7bJY+c3WS+Wq2JhcznOSpn1lSN64yNtHQXboCj3QnUs1EuAZszQG1SHKu5w5ZrlA==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.10.0", + "@docusaurus/types": "3.10.0", + "@docusaurus/utils-common": "3.10.0", + "escape-string-regexp": "^4.0.0", + "execa": "^5.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "github-slugger": "^1.5.0", + "globby": "^11.1.0", + "gray-matter": "^4.0.3", + "jiti": "^1.20.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.5", + "p-queue": "^6.6.2", + "prompts": "^2.4.2", + "resolve-pathname": "^3.0.0", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-common": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.10.0.tgz", + "integrity": "sha512-JyL7sb9QVDgYvudIS81Dv0lsWm7le0vGZSDwsztxWam1SPBqrnkvBy9UYL/amh6pbybkyYTd3CMTkO24oMlCSw==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.10.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-validation": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.0.tgz", + "integrity": "sha512-c+6n2+ZPOJtWWc8Bb/EYdpSDfjYEScdCu9fB/SNjOmSCf1IdVnGf2T53o0tsz0gDRtCL90tifTL0JE/oMuP1Mw==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.10.0", + "@docusaurus/utils": "3.10.0", + "@docusaurus/utils-common": "3.10.0", + "fs-extra": "^11.2.0", + "joi": "^17.9.2", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", + "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", + "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", + "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", + "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", + "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", + "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", + "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.57.2", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", + "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@slorber/remark-comment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", + "integrity": "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.1.0", + "micromark-util-symbol": "^1.0.1" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/gtag.js": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.20.tgz", + "integrity": "sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "license": "MIT" + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-config": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz", + "integrity": "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "^5.1.0" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.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==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/algoliasearch": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.51.0.tgz", + "integrity": "sha512-u3XS8HaTzt5YN90KPsOXMRjYJUMVD1dtr6yi4NXQluMbZ5IjQNBu1MEabdAxFhYtEuexqomPMSmRIhQJUd3QSg==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.17.0", + "@algolia/client-abtesting": "5.51.0", + "@algolia/client-analytics": "5.51.0", + "@algolia/client-common": "5.51.0", + "@algolia/client-insights": "5.51.0", + "@algolia/client-personalization": "5.51.0", + "@algolia/client-query-suggestions": "5.51.0", + "@algolia/client-search": "5.51.0", + "@algolia/ingestion": "1.51.0", + "@algolia/monitoring": "1.51.0", + "@algolia/recommend": "5.51.0", + "@algolia/requester-browser-xhr": "5.51.0", + "@algolia/requester-fetch": "5.51.0", + "@algolia/requester-node-http": "5.51.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.28.2", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.28.2.tgz", + "integrity": "sha512-sexVcXLHrJN54+S0wXD52xV3ySeGZA5T6HMDkb84wT+3UcXCd8af/k2vU5qJTbHv7DoBb4mISJHdyQ2JOo3Aig==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", + "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "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/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combine-promises": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", + "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compressible/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/configstore": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", + "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^6.0.1", + "graceful-fs": "^4.2.6", + "unique-string": "^3.0.0", + "write-file-atomic": "^3.0.3", + "xdg-basedir": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/yeoman/configstore?sponsor=1" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/copy-text-to-clipboard": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz", + "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz", + "integrity": "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==", + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", + "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "cssnano": "^6.0.1", + "jest-worker": "^29.4.3", + "postcss": "^8.4.24", + "schema-utils": "^4.0.1", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.8.0.tgz", + "integrity": "sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "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==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/detect-port": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "license": "MIT", + "dependencies": { + "address": "^1.0.1", + "debug": "4" + }, + "bin": { + "detect": "bin/detect-port.js", + "detect-port": "bin/detect-port.js" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.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==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/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==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", + "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.7", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", + "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.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==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infima": { + "version": "0.2.0-alpha.45", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", + "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", + "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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==", + "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==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.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==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", + "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "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==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.2.tgz", + "integrity": "sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-to-fsa": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-space/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/null-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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/null-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/null-loader/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==", + "license": "MIT" + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-lab-function": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-merge-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", + "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.6.1.tgz", + "integrity": "sha512-yrk74d9EvY+W7+lO9Aj1QmjWY9q5NsKjK2V9drkOPZB/X6KZ0B3igKsHUYakb7oYVhnioWypQX3xGuePf89f3g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.11", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.1", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-position-area-property": "^1.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/postcss-property-rule-prelude-list": "^1.0.0", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.12", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-syntax-descriptor-syntax-production": "^1.0.1", + "@csstools/postcss-system-ui-font-family": "^1.0.0", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.23", + "browserslist": "^4.28.1", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.3", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.6.0", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.12", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.4", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-reduce-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", + "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sort-media-queries": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", + "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", + "license": "MIT", + "dependencies": { + "sort-css-media-queries": "2.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.23" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss-zindex": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", + "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.2.0", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-json-view-lite": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-loadable": { + "name": "@docusaurus/react-loadable", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", + "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-loadable-ssr-addon-v5-slorber": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.3.tgz", + "integrity": "sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.3" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "react-loadable": "*", + "webpack": ">=4.41.1 || 5.x" + } + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-config": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", + "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router": ">=5" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", + "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.2", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.0", + "unified": "^11.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "engines": { + "node": "*" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "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/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", + "peer": true + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", + "license": "MIT", + "dependencies": { + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.3.tgz", + "integrity": "sha512-tAjEd+wt/YwnEbfNB2ht51ybBJxbEWwe5ki/Z//Wh0rpBFTCUSj46GnxUKEWzhfuJTsee8x3lybHxFgUMig2hw==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sort-css-media-queries": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", + "integrity": "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==", + "license": "MIT", + "engines": { + "node": ">= 6.3.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", + "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/thingies": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "0BSD" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "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/update-notifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", + "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^7.0.0", + "chalk": "^5.0.1", + "configstore": "^6.0.0", + "has-yarn": "^3.0.0", + "import-lazy": "^4.0.0", + "is-ci": "^3.0.1", + "is-installed-globally": "^0.4.0", + "is-npm": "^6.0.0", + "is-yarn-global": "^0.4.0", + "latest-version": "^7.0.0", + "pupa": "^3.1.0", + "semver": "^7.3.7", + "semver-diff": "^4.0.0", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/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==", + "license": "MIT" + }, + "node_modules/url-loader/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpack": { + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-middleware/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz", + "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.8.1", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.22.1", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^5.5.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz", + "integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpackbar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", + "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "consola": "^3.2.3", + "figures": "^3.2.0", + "markdown-table": "^2.0.0", + "pretty-time": "^1.1.0", + "std-env": "^3.7.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "webpack": "3 || 4 || 5" + } + }, + "node_modules/webpackbar/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/webpackbar/node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpackbar/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpackbar/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs-site/package.json b/docs-site/package.json new file mode 100644 index 0000000..9fe1dd6 --- /dev/null +++ b/docs-site/package.json @@ -0,0 +1,28 @@ +{ + "name": "metoyou-docs", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "docusaurus start --host 127.0.0.1", + "build": "docusaurus build", + "serve": "docusaurus serve --host 127.0.0.1" + }, + "dependencies": { + "@docusaurus/core": "3.10.0", + "@docusaurus/preset-classic": "3.10.0", + "@mdx-js/react": "^3.1.1", + "clsx": "^2.1.1", + "prism-react-renderer": "^2.4.1", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.10.0", + "@docusaurus/tsconfig": "3.10.0", + "@docusaurus/types": "3.10.0", + "typescript": "~5.9.2" + }, + "overrides": { + "webpack": "5.101.3" + } +} diff --git a/docs-site/sidebars.ts b/docs-site/sidebars.ts new file mode 100644 index 0000000..14ed58d --- /dev/null +++ b/docs-site/sidebars.ts @@ -0,0 +1,62 @@ +import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; + +const sidebars: SidebarsConfig = { + mainSidebar: [ + 'intro', + { + type: 'category', + label: 'User Guide', + items: [ + 'user-guide/first-steps', + 'user-guide/servers-and-channels', + 'user-guide/text-and-direct-messages', + 'user-guide/voice-channels', + 'user-guide/plugins', + 'user-guide/settings', + 'using-metoyou' + ] + }, + { + type: 'category', + label: 'Developer Guide', + items: [ + 'developer/contributing', + 'developer/docusaurus-site', + 'developer/dom-structure', + 'developer/rest-api', + 'developer/llm-plugin-builder-guide', + 'desktop-and-local-api' + ] + }, + { + type: 'category', + label: 'Plugin Development', + items: [ + 'plugin-development/create-a-plugin', + 'plugin-development/manifest', + 'plugin-development/capabilities', + 'plugin-development/api-reference', + { + type: 'category', + label: 'Plugin API Examples', + items: [ + 'plugin-development/api/context-and-logging', + 'plugin-development/api/profile', + 'plugin-development/api/users-and-roles', + 'plugin-development/api/server', + 'plugin-development/api/channels', + 'plugin-development/api/messages-and-typing', + 'plugin-development/api/events', + 'plugin-development/api/message-bus', + 'plugin-development/api/p2p-and-media', + 'plugin-development/api/storage', + 'plugin-development/api/ui' + ] + }, + 'plugin-development/examples' + ] + } + ] +}; + +export default sidebars; diff --git a/docs-site/src/css/custom.css b/docs-site/src/css/custom.css new file mode 100644 index 0000000..7426c2d --- /dev/null +++ b/docs-site/src/css/custom.css @@ -0,0 +1,40 @@ +:root { + --ifm-color-primary: #2f9ab2; + --ifm-color-primary-dark: #2a8ba0; + --ifm-color-primary-darker: #287f94; + --ifm-color-primary-darkest: #216979; + --ifm-color-primary-light: #36abc5; + --ifm-color-primary-lighter: #43b4ce; + --ifm-color-primary-lightest: #6cc5d8; + --ifm-code-font-size: 92%; + --ifm-border-radius: 6px; +} + +[data-theme='dark'] { + --ifm-background-color: #101318; + --ifm-background-surface-color: #171b22; + --ifm-navbar-background-color: #12161d; + --ifm-footer-background-color: #0b0e13; + --ifm-color-primary: #58c4dc; + --ifm-color-primary-dark: #36b7d3; + --ifm-color-primary-darker: #27aeca; + --ifm-color-primary-darkest: #208fa6; + --ifm-color-primary-light: #79d1e3; + --ifm-color-primary-lighter: #8bd7e7; + --ifm-color-primary-lightest: #bde9f1; +} + +.hero--primary { + --ifm-hero-background-color: #151b24; + --ifm-hero-text-color: #f6f8fb; +} + +.theme-doc-markdown table code, +.theme-doc-markdown li code, +.theme-doc-markdown p code { + border: 1px solid var(--ifm-color-emphasis-300); +} + +.plugin-api-table td:first-child { + white-space: nowrap; +} diff --git a/e2e/fixtures/plugins/api-test-plugin/README.md b/e2e/fixtures/plugins/api-test-plugin/README.md new file mode 100644 index 0000000..a9eebab --- /dev/null +++ b/e2e/fixtures/plugins/api-test-plugin/README.md @@ -0,0 +1,3 @@ +# E2E Plugin API Fixture + +This plugin is intentionally tiny. Tests use its manifest to exercise plugin discovery, server support metadata, server data, and plugin event relay APIs without executing plugin code. \ No newline at end of file diff --git a/e2e/fixtures/plugins/api-test-plugin/dist/main.js b/e2e/fixtures/plugins/api-test-plugin/dist/main.js new file mode 100644 index 0000000..b778b6a --- /dev/null +++ b/e2e/fixtures/plugins/api-test-plugin/dist/main.js @@ -0,0 +1,6 @@ +export default { + id: 'e2e.plugin-api', + activate(api) { + api?.logger?.info?.('E2E Plugin API Fixture activated'); + } +}; \ No newline at end of file diff --git a/e2e/fixtures/plugins/api-test-plugin/toju-plugin.json b/e2e/fixtures/plugins/api-test-plugin/toju-plugin.json new file mode 100644 index 0000000..2116a72 --- /dev/null +++ b/e2e/fixtures/plugins/api-test-plugin/toju-plugin.json @@ -0,0 +1,49 @@ +{ + "apiVersion": "1.0.0", + "capabilities": [ + "storage.serverData.read", + "storage.serverData.write", + "events.server.publish", + "events.server.subscribe", + "events.p2p.publish", + "events.p2p.subscribe" + ], + "compatibility": { + "minimumTojuVersion": "1.0.0", + "verifiedTojuVersion": "1.0.0" + }, + "data": [ + { + "key": "settings", + "scope": "server", + "storage": "serverData" + }, + { + "key": "presence", + "scope": "user", + "storage": "serverData" + } + ], + "description": "Fixture plugin used by automated tests for plugin support APIs.", + "entrypoint": "./dist/main.js", + "events": [ + { + "direction": "serverRelay", + "eventName": "e2e:relay", + "maxPayloadBytes": 2048, + "scope": "server" + }, + { + "direction": "p2pHint", + "eventName": "e2e:p2p", + "maxPayloadBytes": 512, + "scope": "user" + } + ], + "id": "e2e.plugin-api", + "kind": "client", + "readme": "./README.md", + "schemaVersion": 1, + "title": "E2E Plugin API Fixture", + "version": "1.0.0" +} \ No newline at end of file diff --git a/e2e/helpers/plugin-api-test-fixture.ts b/e2e/helpers/plugin-api-test-fixture.ts new file mode 100644 index 0000000..d95bda2 --- /dev/null +++ b/e2e/helpers/plugin-api-test-fixture.ts @@ -0,0 +1,42 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +export const TEST_PLUGIN_FIXTURE_DIR = join(__dirname, '..', 'fixtures', 'plugins', 'api-test-plugin'); +export const TEST_PLUGIN_ID = 'e2e.plugin-api'; +export const TEST_PLUGIN_RELAY_EVENT = 'e2e:relay'; +export const TEST_PLUGIN_P2P_EVENT = 'e2e:p2p'; + +export interface PluginApiTestManifestEvent { + direction: 'clientToServer' | 'serverRelay' | 'p2pHint'; + eventName: string; + maxPayloadBytes?: number; + scope: 'server' | 'channel' | 'user' | 'plugin'; +} + +export interface PluginApiTestManifest { + description: string; + events: PluginApiTestManifestEvent[]; + id: string; + title: string; + version: string; +} + +export async function readPluginApiTestManifest(): Promise { + const manifestPath = join(TEST_PLUGIN_FIXTURE_DIR, 'toju-plugin.json'); + const manifestText = await readFile(manifestPath, 'utf8'); + + return JSON.parse(manifestText) as PluginApiTestManifest; +} + +export function getPluginApiTestEvent( + manifest: PluginApiTestManifest, + eventName: string +): PluginApiTestManifestEvent { + const eventDefinition = manifest.events.find((event) => event.eventName === eventName); + + if (!eventDefinition) { + throw new Error(`Expected fixture plugin to define ${eventName}`); + } + + return eventDefinition; +} diff --git a/e2e/helpers/seed-test-endpoint.ts b/e2e/helpers/seed-test-endpoint.ts index 9713d6a..81332b6 100644 --- a/e2e/helpers/seed-test-endpoint.ts +++ b/e2e/helpers/seed-test-endpoint.ts @@ -69,6 +69,7 @@ function applySeededEndpointStorageState(storageState: SeededEndpointStorageStat 'toju-primary', 'toju-sweden' ])); + storage.setItem('metoyou_general_settings', generalSettings); if (currentUserId) { diff --git a/e2e/pages/login.page.ts b/e2e/pages/login.page.ts index 6d58d9c..992e753 100644 --- a/e2e/pages/login.page.ts +++ b/e2e/pages/login.page.ts @@ -10,7 +10,9 @@ export class LoginPage { readonly registerLink: Locator; constructor(private page: Page) { - this.form = page.locator('#login-username').locator('xpath=ancestor::div[contains(@class, "space-y-3")]').first(); + this.form = page.locator('#login-username').locator('xpath=ancestor::div[contains(@class, "space-y-3")]') + .first(); + this.usernameInput = page.locator('#login-username'); this.passwordInput = page.locator('#login-password'); this.serverSelect = page.locator('#login-server'); diff --git a/e2e/pages/server-search.page.ts b/e2e/pages/server-search.page.ts index 82fadb1..9c1777a 100644 --- a/e2e/pages/server-search.page.ts +++ b/e2e/pages/server-search.page.ts @@ -79,12 +79,19 @@ export class ServerSearchPage { await this.page.getByRole('button', { name }).click(); } - async joinServerFromSearch(name: string) { + async joinServerFromSearch(name: string, options: { acceptPluginDownloads?: boolean } = {}) { await this.searchInput.fill(name); const serverCard = this.page.locator('div[title]', { hasText: name }).first(); await expect(serverCard).toBeVisible({ timeout: 15_000 }); await serverCard.dblclick(); + + if (options.acceptPluginDownloads) { + const pluginConsentDialog = this.page.getByRole('dialog', { name: /uses plugins/ }); + + await expect(pluginConsentDialog).toBeVisible({ timeout: 20_000 }); + await pluginConsentDialog.getByRole('button', { name: 'Accept and join' }).click(); + } } } diff --git a/e2e/tests/auth/user-session-data-isolation.spec.ts b/e2e/tests/auth/user-session-data-isolation.spec.ts index e017969..1aa6424 100644 --- a/e2e/tests/auth/user-session-data-isolation.spec.ts +++ b/e2e/tests/auth/user-session-data-isolation.spec.ts @@ -25,10 +25,7 @@ interface PersistentClient { userDataDir: string; } -const CLIENT_LAUNCH_ARGS = [ - '--use-fake-device-for-media-stream', - '--use-fake-ui-for-media-stream' -]; +const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream']; test.describe('User session data isolation', () => { test.describe.configure({ timeout: 240_000 }); @@ -43,6 +40,7 @@ test.describe('User session data isolation', () => { }; const aliceServerName = `Alice Session Server ${suffix}`; const aliceMessage = `Alice persisted message ${suffix}`; + let client: PersistentClient | null = null; try { @@ -82,6 +80,7 @@ test.describe('User session data isolation', () => { const bobServerName = `Bob Private Server ${suffix}`; const aliceMessage = `Alice history ${suffix}`; const bobMessage = `Bob history ${suffix}`; + let client: PersistentClient | null = null; try { @@ -136,7 +135,7 @@ async function launchPersistentClient(userDataDir: string, testServerPort: numbe await installTestServerEndpoint(context, testServerPort); - const page = context.pages()[0] ?? await context.newPage(); + const page = context.pages()[0] ?? (await context.newPage()); return { context, @@ -202,6 +201,7 @@ async function createServerAndSendMessage(page: Page, serverName: string, messag await searchPage.createServer(serverName, { description: `User session isolation coverage for ${serverName}` }); + await expect(page).toHaveURL(/\/room\//, { timeout: 15_000 }); await messagesPage.sendMessage(messageText); @@ -209,11 +209,15 @@ async function createServerAndSendMessage(page: Page, serverName: string, messag } async function expectSavedRoomAndHistory(page: Page, roomName: string, messageText: string): Promise { - const roomButton = getSavedRoomButton(page, roomName); + const railRoomButton = getRailSavedRoomButton(page, roomName); const messagesPage = new ChatMessagesPage(page); - await expect(roomButton).toBeVisible({ timeout: 20_000 }); - await roomButton.click(); + await expect(railRoomButton).toBeVisible({ timeout: 20_000 }); + await page.goto('/search', { waitUntil: 'domcontentloaded' }); + const searchRoomButton = getSearchSavedRoomButton(page, roomName); + + await expect(searchRoomButton).toBeVisible({ timeout: 20_000 }); + await searchRoomButton.click(); await expect(page).toHaveURL(/\/room\//, { timeout: 20_000 }); await expect(messagesPage.getMessageItemByText(messageText)).toBeVisible({ timeout: 20_000 }); } @@ -230,17 +234,29 @@ async function expectBlankSlate(page: Page, hiddenRoomNames: string[]): Promise< } async function expectSavedRoomVisible(page: Page, roomName: string): Promise { - await expect(getSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 }); + await expect(getRailSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 }); + await page.goto('/search', { waitUntil: 'domcontentloaded' }); + await expect(getSearchSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 }); } async function expectSavedRoomHidden(page: Page, roomName: string): Promise { - await expect(getSavedRoomButton(page, roomName)).toHaveCount(0); + await expect(getRailSavedRoomButton(page, roomName)).toHaveCount(0); + + if (!page.url().includes('/search')) { + await page.goto('/search', { waitUntil: 'domcontentloaded' }); + } + + await expect(getSearchSavedRoomButton(page, roomName)).toHaveCount(0); } -function getSavedRoomButton(page: Page, roomName: string) { +function getRailSavedRoomButton(page: Page, roomName: string) { return page.locator(`button[title="${roomName}"]`).first(); } +function getSearchSavedRoomButton(page: Page, roomName: string) { + return page.locator('app-server-search').getByRole('button', { name: roomName, exact: true }); +} + async function retryTransientNavigation(navigate: () => Promise, attempts = 4): Promise { let lastError: unknown; @@ -259,11 +275,10 @@ async function retryTransientNavigation(navigate: () => Promise, attempts } } - throw lastError instanceof Error - ? lastError - : new Error(`Navigation failed after ${attempts} attempts`); + throw lastError instanceof Error ? lastError : new Error(`Navigation failed after ${attempts} attempts`); } function uniqueName(prefix: string): string { - return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; -} \ No newline at end of file + return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36) + .slice(2, 8)}`; +} diff --git a/e2e/tests/chat/server-icon-sync.spec.ts b/e2e/tests/chat/server-icon-sync.spec.ts new file mode 100644 index 0000000..752c010 --- /dev/null +++ b/e2e/tests/chat/server-icon-sync.spec.ts @@ -0,0 +1,503 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + chromium, + type BrowserContext, + type Locator, + type Page, + type Route +} from '@playwright/test'; +import { test, expect } from '../../fixtures/multi-client'; +import { installTestServerEndpoint } from '../../helpers/seed-test-endpoint'; +import { installWebRTCTracking } from '../../helpers/webrtc-helpers'; +import { LoginPage } from '../../pages/login.page'; +import { RegisterPage } from '../../pages/register.page'; +import { ServerSearchPage } from '../../pages/server-search.page'; +import { ChatMessagesPage } from '../../pages/chat-messages.page'; + +interface TestUser { + displayName: string; + password: string; + username: string; +} + +interface ImageUploadPayload { + buffer: Buffer; + dataUrl: string; + mimeType: string; + name: string; +} + +interface PersistentClient { + context: BrowserContext; + page: Page; + user: TestUser; + userDataDir: string; +} + +const STATIC_GIF_BASE64 = 'R0lGODlhAQABAPAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=='; +const GIF_FRAME_MARKER = Buffer.from([ + 0x21, + 0xf9, + 0x04 +]); +const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream']; +const SERVER_ICON_SYNC_TIMEOUT_MS = 45_000; + +test.describe('Server icon sync', () => { + test.describe.configure({ timeout: 240_000 }); + + test('loads the chat-server image for online, late-joining, restarted, and discovery users', async ({ testServer }) => { + const suffix = uniqueName('server-icon'); + const serverName = `Icon Sync Server ${suffix}`; + const icon = buildGifUpload('server-icon'); + const aliceUser: TestUser = { + username: `alice_${suffix}`, + displayName: 'Alice', + password: 'TestPass123!' + }; + const bobUser: TestUser = { + username: `bob_${suffix}`, + displayName: 'Bob', + password: 'TestPass123!' + }; + const carolUser: TestUser = { + username: `carol_${suffix}`, + displayName: 'Carol', + password: 'TestPass123!' + }; + const daveUser: TestUser = { + username: `dave_${suffix}`, + displayName: 'Dave', + password: 'TestPass123!' + }; + const clients: PersistentClient[] = []; + + try { + const alice = await createPersistentClient(aliceUser, testServer.port); + const bob = await createPersistentClient(bobUser, testServer.port); + + clients.push(alice, bob); + + await test.step('Alice creates a server and Bob joins before the icon changes', async () => { + await registerUser(alice); + await registerUser(bob); + + await new ServerSearchPage(alice.page).createServer(serverName, { + description: 'Server icon synchronization E2E coverage' + }); + + await expect(alice.page).toHaveURL(/\/room\//, { timeout: 15_000 }); + + await joinServerFromSearch(bob.page, serverName); + await waitForRoomReady(alice.page); + await waitForRoomReady(bob.page); + await waitForConnectedPeerCount(alice.page, 1); + await waitForConnectedPeerCount(bob.page, 1); + }); + + const roomUrl = alice.page.url(); + + await test.step('Alice uploads a server icon and sees it in every owner-facing place', async () => { + await uploadServerIconFromSettings(alice.page, serverName, icon); + + await expectServerSettingsIcon(alice.page, serverName, icon.dataUrl); + await closeSettingsModal(alice.page); + await expectRoomHeaderIcon(alice.page, serverName, icon.dataUrl); + await expectRailIcon(alice.page, serverName, icon.dataUrl); + }); + + await test.step('Bob was online during the change and receives the icon live', async () => { + await expectRoomHeaderIcon(bob.page, serverName, icon.dataUrl); + await expectRailIcon(bob.page, serverName, icon.dataUrl); + }); + + const carol = await createPersistentClient(carolUser, testServer.port); + + clients.push(carol); + + await test.step('Carol joins after the change and loads the existing server icon', async () => { + await registerUser(carol); + await joinServerFromSearch(carol.page, serverName); + await waitForRoomReady(carol.page); + await waitForConnectedPeerCount(alice.page, 2); + + await expectRoomHeaderIcon(carol.page, serverName, icon.dataUrl); + await expectRailIcon(carol.page, serverName, icon.dataUrl); + }); + + await test.step('Bob keeps the server icon after a full app restart', async () => { + await restartPersistentClient(bob, testServer.port); + await openRoomAfterRestart(bob, roomUrl); + + await expectRoomHeaderIcon(bob.page, serverName, icon.dataUrl); + await expectRailIcon(bob.page, serverName, icon.dataUrl); + }); + + const dave = await createPersistentClient(daveUser, testServer.port); + + clients.push(dave); + + await test.step('Dave has not joined, but discovery loads the icon through a temporary peer sync', async () => { + await registerUser(dave); + await stripServerIconFromDirectorySearch(dave.page, serverName); + await dave.page.goto('/search', { waitUntil: 'domcontentloaded' }); + await new ServerSearchPage(dave.page).searchInput.fill(serverName); + + await expectSearchResultIcon(dave.page, serverName, icon.dataUrl); + await expect(dave.page).toHaveURL(/\/search/); + }); + } finally { + await Promise.all( + clients.map(async (client) => { + await closePersistentClient(client); + await rm(client.userDataDir, { recursive: true, force: true }); + }) + ); + } + }); +}); + +async function createPersistentClient(user: TestUser, testServerPort: number): Promise { + const userDataDir = await mkdtemp(join(tmpdir(), 'metoyou-server-icon-e2e-')); + const session = await launchPersistentSession(userDataDir, testServerPort); + + return { + context: session.context, + page: session.page, + user, + userDataDir + }; +} + +async function restartPersistentClient(client: PersistentClient, testServerPort: number): Promise { + await closePersistentClient(client); + + const session = await launchPersistentSession(client.userDataDir, testServerPort); + + client.context = session.context; + client.page = session.page; +} + +async function closePersistentClient(client: PersistentClient): Promise { + try { + await client.context.close(); + } catch { + // Ignore repeated cleanup attempts during finally. + } +} + +async function launchPersistentSession(userDataDir: string, testServerPort: number): Promise<{ context: BrowserContext; page: Page }> { + const context = await chromium.launchPersistentContext(userDataDir, { + args: CLIENT_LAUNCH_ARGS, + baseURL: 'http://localhost:4200', + permissions: ['microphone', 'camera'] + }); + + await installTestServerEndpoint(context, testServerPort); + + const page = context.pages()[0] ?? (await context.newPage()); + + await installWebRTCTracking(page); + + return { context, page }; +} + +async function registerUser(client: PersistentClient): Promise { + const registerPage = new RegisterPage(client.page); + + await retryTransientNavigation(() => registerPage.goto()); + await registerPage.register(client.user.username, client.user.displayName, client.user.password); + await expect(client.page).toHaveURL(/\/search/, { timeout: 15_000 }); +} + +async function joinServerFromSearch(page: Page, serverName: string): Promise { + await new ServerSearchPage(page).joinServerFromSearch(serverName); + await expect(page).toHaveURL(/\/room\//, { timeout: 15_000 }); +} + +async function openRoomAfterRestart(client: PersistentClient, roomUrl: string): Promise { + await retryTransientNavigation(() => client.page.goto(roomUrl, { waitUntil: 'domcontentloaded' })); + + if (client.page.url().includes('/login')) { + const loginPage = new LoginPage(client.page); + + await loginPage.login(client.user.username, client.user.password); + await expect(client.page).toHaveURL(/\/(search|room)\//, { timeout: 15_000 }); + await client.page.goto(roomUrl, { waitUntil: 'domcontentloaded' }); + } + + await waitForRoomReady(client.page); +} + +async function uploadServerIconFromSettings(page: Page, serverName: string, icon: ImageUploadPayload): Promise { + await openServerSettings(page, serverName); + + const fileInput = page.locator('#server-icon-upload'); + + await expect(fileInput).toBeAttached({ timeout: 10_000 }); + await fileInput.setInputFiles({ + name: icon.name, + mimeType: icon.mimeType, + buffer: icon.buffer + }); +} + +async function openServerSettings(page: Page, serverName: string): Promise { + await page.locator('app-title-bar button[title="Menu"]').click(); + + const titleBarMenu = page.locator('app-title-bar .absolute.right-0.top-full').first(); + + await expect(titleBarMenu).toBeVisible({ timeout: 5_000 }); + await titleBarMenu.getByRole('button', { name: 'Settings' }).click(); + + const dialog = page.locator('app-settings-modal'); + const serverSettingsTitle = dialog.getByRole('heading', { name: 'Server Settings' }); + + try { + await expect(serverSettingsTitle).toBeVisible({ timeout: 2_000 }); + } catch { + await openSettingsModalThroughAngularDevMode(page); + await expect(serverSettingsTitle).toBeVisible({ timeout: 10_000 }); + } + + const serverSelect = dialog.locator('select').first(); + + if ((await serverSelect.count()) > 0) { + await expect(serverSelect).toContainText(serverName, { timeout: 10_000 }); + } + + await dialog.getByRole('button', { name: 'Server', exact: true }).click(); + await expect(page.locator('app-server-settings')).toBeVisible({ timeout: 10_000 }); +} + +async function openSettingsModalThroughAngularDevMode(page: Page): Promise { + await page.evaluate(() => { + interface SettingsModalComponentHandle { + modal?: { + open: (page: string) => void; + }; + } + interface AngularDebugApi { + getComponent: (element: Element) => SettingsModalComponentHandle; + applyChanges?: (component: SettingsModalComponentHandle) => void; + } + + const host = document.querySelector('app-settings-modal'); + const debugApi = (window as Window & { ng?: AngularDebugApi }).ng; + const component = host && debugApi?.getComponent(host); + + if (!component?.modal?.open) { + throw new Error('Angular debug API could not open settings modal'); + } + + component.modal.open('server'); + debugApi.applyChanges?.(component); + }); +} + +async function closeSettingsModal(page: Page): Promise { + await page.keyboard.press('Escape'); + await expect(page.locator('app-settings-modal').getByRole('heading', { name: 'Settings', exact: true })).not.toBeVisible({ timeout: 10_000 }); +} + +async function stripServerIconFromDirectorySearch(page: Page, serverName: string): Promise { + await page.route('**/api/servers**', async (route: Route) => { + const response = await route.fetch(); + const contentType = response.headers()['content-type'] ?? ''; + + if (!contentType.includes('application/json')) { + await route.fulfill({ response }); + return; + } + + const body = await response.json(); + + if (!body || !Array.isArray(body.servers)) { + await route.fulfill({ response, json: body }); + return; + } + + await route.fulfill({ + response, + json: { + ...body, + servers: body.servers.map((server: Record) => { + if (server['name'] !== serverName) { + return server; + } + + const { icon: _icon, ...serverWithoutIcon } = server; + + return serverWithoutIcon; + }) + } + }); + }); +} + +async function waitForRoomReady(page: Page): Promise { + const messagesPage = new ChatMessagesPage(page); + + await messagesPage.waitForReady(); + await expect(page.locator('app-rooms-side-panel').last()).toBeVisible({ timeout: 15_000 }); +} + +async function waitForConnectedPeerCount(page: Page, count: number, timeout = 30_000): Promise { + await page.waitForFunction( + (expectedCount) => { + const connections = + ( + window as { + __rtcConnections?: RTCPeerConnection[]; + } + ).__rtcConnections ?? []; + + return connections.filter((connection) => connection.connectionState === 'connected').length >= expectedCount; + }, + count, + { timeout } + ); +} + +async function retryTransientNavigation(navigate: () => Promise, attempts = 4): Promise { + let lastError: unknown; + + for (let attempt = 1; attempt <= attempts; attempt++) { + try { + return await navigate(); + } catch (error) { + lastError = error; + + const message = error instanceof Error ? error.message : String(error); + const isTransientNavigationError = message.includes('ERR_EMPTY_RESPONSE') || message.includes('ERR_CONNECTION_RESET'); + + if (!isTransientNavigationError || attempt === attempts) { + throw error; + } + } + } + + throw lastError instanceof Error ? lastError : new Error(`Navigation failed after ${attempts} attempts`); +} + +async function expectServerSettingsIcon(page: Page, serverName: string, expectedDataUrl: string): Promise { + const settingsPanel = page.locator('app-server-settings'); + const image = settingsPanel.locator('[style*="background-image"]').first(); + + await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'settings server icon'); +} + +async function expectRoomHeaderIcon(page: Page, serverName: string, expectedDataUrl: string): Promise { + const channelsPanel = page.locator('app-rooms-side-panel').first(); + const image = channelsPanel.locator('[style*="background-image"]').first(); + + await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'room header server icon'); +} + +async function expectRailIcon(page: Page, serverName: string, expectedDataUrl: string): Promise { + const image = page.locator(`app-servers-rail button[title="${serverName}"] [style*="background-image"]`).first(); + + await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'servers rail icon'); +} + +async function expectSearchResultIcon(page: Page, serverName: string, expectedDataUrl: string): Promise { + const serverCard = page.locator('app-server-search div[title]', { hasText: serverName }).first(); + const image = serverCard.locator('[style*="background-image"]').first(); + + await expect(serverCard).toBeVisible({ timeout: 20_000 }); + await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'search result server icon'); +} + +async function expectBackgroundImageLoadedWithUrl(image: Locator, expectedDataUrl: string, label: string): Promise { + await expect + .poll( + async () => { + if ((await image.count()) === 0) { + return null; + } + + return image.evaluate((element) => getComputedStyle(element).backgroundImage); + }, + { + timeout: SERVER_ICON_SYNC_TIMEOUT_MS, + message: `${label} background should update` + } + ) + .toContain(expectedDataUrl); + + await expect + .poll( + async () => { + if ((await image.count()) === 0) { + return false; + } + + return image.evaluate( + (element) => + new Promise((resolve) => { + const backgroundImage = getComputedStyle(element).backgroundImage; + const match = /^url\("?(.*?)"?\)$/.exec(backgroundImage); + const img = new Image(); + + if (!match?.[1]) { + resolve(false); + return; + } + + img.onload = () => resolve(img.naturalWidth > 0 && img.naturalHeight > 0); + img.onerror = () => resolve(false); + img.src = match[1]; + }) + ); + }, + { + timeout: SERVER_ICON_SYNC_TIMEOUT_MS, + message: `${label} should load` + } + ) + .toBe(true); +} + +function buildGifUpload(label: string): ImageUploadPayload { + const baseGif = Buffer.from(STATIC_GIF_BASE64, 'base64'); + const frameStart = baseGif.indexOf(GIF_FRAME_MARKER); + + if (frameStart < 0) { + throw new Error('Failed to locate GIF frame marker for server icon payload'); + } + + const header = baseGif.subarray(0, frameStart); + const frame = baseGif.subarray(frameStart, baseGif.length - 1); + const commentData = Buffer.from(label, 'ascii'); + const commentExtension = Buffer.concat([ + Buffer.from([ + 0x21, + 0xfe, + commentData.length + ]), + commentData, + Buffer.from([0x00]) + ]); + const buffer = Buffer.concat([ + header, + commentExtension, + frame, + frame, + Buffer.from([0x3b]) + ]); + const base64 = buffer.toString('base64'); + + return { + buffer, + dataUrl: `data:image/gif;base64,${base64}`, + mimeType: 'image/gif', + name: `server-icon-${label}.gif` + }; +} + +function uniqueName(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36) + .slice(2, 8)}`; +} diff --git a/e2e/tests/plugins/plugin-api-two-users.spec.ts b/e2e/tests/plugins/plugin-api-two-users.spec.ts new file mode 100644 index 0000000..6a19c96 --- /dev/null +++ b/e2e/tests/plugins/plugin-api-two-users.spec.ts @@ -0,0 +1,186 @@ +import { type Page } from '@playwright/test'; +import { + expect, + test, + type Client +} from '../../fixtures/multi-client'; +import { ChatMessagesPage } from '../../pages/chat-messages.page'; +import { ChatRoomPage } from '../../pages/chat-room.page'; +import { RegisterPage } from '../../pages/register.page'; +import { ServerSearchPage } from '../../pages/server-search.page'; + +const PLUGIN_SOURCE_URL = 'http://localhost:4200/plugins/e2e-plugin-source.json'; +const PLUGIN_TITLE = 'E2E All API Plugin'; +const EDITED_MESSAGE = 'Plugin API edited message'; +const ORIGINAL_MESSAGE = 'Plugin API original message'; +const DELETED_MESSAGE = 'Plugin API deleted message'; +const DELETED_MESSAGE_CONTENT = '[Message deleted]'; +const PLUGIN_BOT_MESSAGE = 'Plugin bot message from all-api fixture'; +const CUSTOM_EMBED_TEXT = 'E2E custom embed: Plugin API custom embed'; +const SOUND_BOARD_TEXT = 'E2E soundboard ready'; +const SOUND_BOARD_LABEL = 'E2E Soundboard'; +const SOUND_BOARD_PLAYED_MESSAGE = 'E2E soundboard played Airhorn to voice channel'; +const VOICE_CHANNEL = 'Plugin Voice'; + +test.describe('Plugin API multi-user runtime', () => { + test.describe.configure({ timeout: 180_000 }); + + test('runs chat, embed, soundboard, and profile APIs between two users', async ({ createClient }) => { + const scenario = await createPluginApiScenario(createClient); + + await test.step('Alice has the server plugin active', async () => { + await expect(soundboardComposerButton(scenario.alice.page)).toBeVisible({ timeout: 20_000 }); + await expect(scenario.alice.page.getByText(SOUND_BOARD_TEXT, { exact: true })).toBeVisible({ timeout: 20_000 }); + await expect(scenario.alice.page.getByTestId('e2e-plugin-owned-dom')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin'); + }); + + await test.step('Activate the server plugin for Bob as the embed/soundboard receiver', async () => { + await installGrantAndActivatePlugin(scenario.bob.page, false); + await closeSettingsModal(scenario.bob.page); + await expect(soundboardComposerButton(scenario.bob.page)).toBeVisible({ timeout: 20_000 }); + await expect(scenario.bob.page.getByText(SOUND_BOARD_TEXT, { exact: true })).toBeVisible({ timeout: 20_000 }); + await expect(scenario.bob.page.getByTestId('e2e-plugin-owned-dom')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin'); + }); + + await test.step('Alice opens the plugin soundboard modal and plays a sound to voice', async () => { + await soundboardComposerButton(scenario.alice.page).click(); + await expect(scenario.alice.page.getByRole('dialog', { name: SOUND_BOARD_LABEL })).toBeVisible({ timeout: 20_000 }); + await expect(scenario.alice.page.getByTestId('e2e-soundboard-modal')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin'); + await scenario.alice.page.getByRole('button', { name: 'Play airhorn to voice' }).click(); + await expect(scenario.alice.page.getByTestId('e2e-soundboard-status')).toHaveText(SOUND_BOARD_PLAYED_MESSAGE, { timeout: 20_000 }); + }); + + await test.step('Bob receives messages sent and edited by Alice through the plugin API', async () => { + await expect(scenario.bobMessages.getMessageItemByText(EDITED_MESSAGE)).toBeVisible({ timeout: 30_000 }); + await expect(scenario.bobMessages.getMessageItemByText(ORIGINAL_MESSAGE)).toHaveCount(0); + await expect(scenario.bob.page.getByText('(edited)')).toBeVisible({ timeout: 20_000 }); + }); + + await test.step('Bob sees plugin API deletion state and plugin-user messages', async () => { + await expect(scenario.bobMessages.getMessageItemByText(DELETED_MESSAGE_CONTENT)).toBeVisible({ timeout: 30_000 }); + await expect(scenario.bobMessages.getMessageItemByText(DELETED_MESSAGE)).toHaveCount(0); + await expect(scenario.bobMessages.getMessageItemByText(PLUGIN_BOT_MESSAGE)).toBeVisible({ timeout: 30_000 }); + await expect(scenario.bobMessages.getMessageItemByText(SOUND_BOARD_PLAYED_MESSAGE)).toBeVisible({ timeout: 30_000 }); + }); + + await test.step('Bob renders Alice custom embed through the plugin embed API', async () => { + await expect(scenario.bob.page.getByTestId('plugin-message-embeds')).toContainText(CUSTOM_EMBED_TEXT, { timeout: 30_000 }); + }); + + await test.step('Bob sees Alice profile name changed by the plugin API', async () => { + await expect(scenario.bobMessages.getMessageItemByText(EDITED_MESSAGE)).toContainText('Alice Plugin Renamed', { timeout: 30_000 }); + }); + }); +}); + +interface PluginApiScenario { + alice: Client; + aliceRoom: ChatRoomPage; + bob: Client; + bobRoom: ChatRoomPage; + aliceMessages: ChatMessagesPage; + bobMessages: ChatMessagesPage; +} + +async function createPluginApiScenario(createClient: () => Promise): Promise { + const suffix = uniqueName('plugin-api'); + const serverName = `Plugin API Server ${suffix}`; + const alice = await createClient(); + const bob = await createClient(); + + await registerUser(alice.page, `alice_${suffix}`, 'Alice'); + await registerUser(bob.page, `bob_${suffix}`, 'Bob'); + + const aliceSearch = new ServerSearchPage(alice.page); + + await aliceSearch.createServer(serverName, { description: 'Two-user plugin API E2E coverage' }); + await expect(alice.page).toHaveURL(/\/room\//, { timeout: 30_000 }); + + const aliceRoom = new ChatRoomPage(alice.page); + + await aliceRoom.ensureVoiceChannelExists(VOICE_CHANNEL); + await installGrantAndActivatePlugin(alice.page, true); + await closeSettingsModal(alice.page); + await expect(soundboardComposerButton(alice.page)).toBeVisible({ timeout: 20_000 }); + + const bobSearch = new ServerSearchPage(bob.page); + + await bobSearch.joinServerFromSearch(serverName, { acceptPluginDownloads: true }); + await expect(bob.page).toHaveURL(/\/room\//, { timeout: 30_000 }); + + const bobRoom = new ChatRoomPage(bob.page); + + await aliceRoom.joinVoiceChannel(VOICE_CHANNEL); + await bobRoom.joinVoiceChannel(VOICE_CHANNEL); + await expect(aliceRoom.voiceControls).toBeVisible({ timeout: 30_000 }); + await expect(bobRoom.voiceControls).toBeVisible({ timeout: 30_000 }); + + const aliceMessages = new ChatMessagesPage(alice.page); + const bobMessages = new ChatMessagesPage(bob.page); + + await aliceMessages.waitForReady(); + await bobMessages.waitForReady(); + await expect(alice.page.locator('[data-testid^="room-user-card-"]', { hasText: 'Bob' })).toBeVisible({ timeout: 30_000 }); + await expect(bob.page.locator('[data-testid^="room-user-card-"]', { hasText: 'Alice' })).toBeVisible({ timeout: 30_000 }); + + return { + alice, + aliceRoom, + bob, + bobRoom, + aliceMessages, + bobMessages + }; +} + +async function registerUser(page: Page, username: string, displayName: string): Promise { + const registerPage = new RegisterPage(page); + + await registerPage.goto(); + await registerPage.register(username, displayName, 'TestPass123!'); + await expect(page).toHaveURL(/\/search/, { timeout: 30_000 }); +} + +async function installGrantAndActivatePlugin(page: Page, installFromStore: boolean): Promise { + await page.getByRole('button', { name: 'Plugin Store' }).click(); + await expect(page).toHaveURL(/\/plugin-store/, { timeout: 20_000 }); + await expect(page.getByTestId('plugin-store-page')).toBeVisible({ timeout: 20_000 }); + + if (installFromStore) { + await page.getByLabel('Plugin source manifest URL').fill(PLUGIN_SOURCE_URL); + await page.getByRole('button', { name: 'Add Source' }).click(); + await expect(page.getByRole('heading', { name: PLUGIN_TITLE })).toBeVisible({ timeout: 20_000 }); + await page.getByRole('button', { exact: true, name: /^(Install|Install to Server)$/ }).click(); + await expect(page.getByRole('dialog', { name: PLUGIN_TITLE })).toBeVisible({ timeout: 10_000 }); + await page.getByRole('button', { name: 'Install and Activate' }).click(); + await expect(page.locator('article', { hasText: PLUGIN_TITLE }).getByText('Installed')).toBeVisible({ timeout: 20_000 }); + } + + await page.getByRole('button', { name: 'Manage Plugins' }).click(); + await expect(page.getByTestId('plugin-manager')).toBeVisible({ timeout: 20_000 }); + await expect(page.locator('article', { hasText: PLUGIN_TITLE })).toBeVisible({ timeout: 20_000 }); + await page.locator('article', { hasText: PLUGIN_TITLE }) + .getByRole('button', { name: 'Select' }) + .click(); + + await page.getByRole('button', { name: 'Grant all requested' }).click(); + await page.getByRole('button', { name: 'Activate ready plugins' }).click(); + await expect(page.locator('article', { hasText: PLUGIN_TITLE }).getByText('ready', { exact: true })).toBeVisible({ timeout: 30_000 }); + await page.getByRole('button', { name: 'Logs' }).click(); + await expect(page.getByText('all-api plugin completed')).toBeVisible({ timeout: 30_000 }); +} + +async function closeSettingsModal(page: Page): Promise { + await page.keyboard.press('Escape'); + await expect(page.getByTestId('plugin-manager')).toHaveCount(0); +} + +function uniqueName(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36) + .slice(2, 8)}`; +} + +function soundboardComposerButton(page: Page) { + return page.locator('app-chat-message-composer') + .getByRole('button', { exact: true, name: SOUND_BOARD_LABEL }); +} diff --git a/e2e/tests/plugins/plugin-manager-ui.spec.ts b/e2e/tests/plugins/plugin-manager-ui.spec.ts new file mode 100644 index 0000000..5910fd8 --- /dev/null +++ b/e2e/tests/plugins/plugin-manager-ui.spec.ts @@ -0,0 +1,93 @@ +import { expect, test } from '../../fixtures/multi-client'; +import { RegisterPage } from '../../pages/register.page'; +import { ServerSearchPage } from '../../pages/server-search.page'; + +test.describe('Plugin manager UI', () => { + test.describe.configure({ timeout: 180_000 }); + + test('installs, grants, activates, and logs an all-API test plugin', async ({ createClient }) => { + const client = await createClient(); + const { page } = client; + const suffix = Date.now(); + const register = new RegisterPage(page); + const search = new ServerSearchPage(page); + + await test.step('Register user and create server context', async () => { + await register.goto(); + await register.register(`plugin_${suffix}`, 'Plugin Tester', 'TestPass123!'); + await expect(page.getByPlaceholder('Search servers and users...')).toBeVisible({ timeout: 30_000 }); + await search.createServer(`Plugin API Server ${suffix}`, { + description: 'Plugin manager UI E2E coverage' + }); + + await expect(page).toHaveURL(/\/room\//, { timeout: 30_000 }); + }); + + await test.step('Open visible Plugin Store button', async () => { + await page.getByRole('button', { name: 'Plugin Store' }).click(); + await expect(page).toHaveURL(/\/plugin-store/, { timeout: 10_000 }); + await expect(page.getByTestId('plugin-store-page')).toBeVisible({ timeout: 10_000 }); + }); + + await test.step('Install fixture plugin from source manifest', async () => { + await page.getByLabel('Plugin source manifest URL').fill('http://localhost:4200/plugins/e2e-plugin-source.json'); + await page.getByRole('button', { name: 'Add Source' }).click(); + await expect(page.getByRole('heading', { name: 'E2E All API Plugin' })).toBeVisible({ timeout: 15_000 }); + await page.getByRole('button', { name: 'Readme' }).click(); + await expect(page.getByText('Fixture plugin for Playwright coverage.')).toBeVisible({ timeout: 10_000 }); + await page.getByRole('button', { exact: true, name: /^(Install|Install to Server)$/ }).click(); + const installDialog = page.getByRole('dialog', { name: 'E2E All API Plugin' }); + + await expect(installDialog).toBeVisible({ timeout: 10_000 }); + await expect(installDialog.getByText('Install to server', { exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Install and Activate' }).click(); + await expect(page.locator('article', { hasText: 'E2E All API Plugin' }).getByText('Installed')).toBeVisible({ timeout: 10_000 }); + }); + + await test.step('Open plugin manager from the store page', async () => { + await page.getByRole('button', { name: 'Manage Plugins' }).click(); + await expect(page.getByTestId('plugin-manager')).toBeVisible({ timeout: 10_000 }); + await expect(page.getByTestId('plugin-manager').getByRole('heading', { name: 'Server plugins' })).toBeVisible(); + await expect(page.getByText('E2E All API Plugin')).toBeVisible(); + }); + + await test.step('Grant capabilities and activate runtime', async () => { + const manager = page.getByTestId('plugin-manager'); + const pluginCard = manager.locator('article', { hasText: 'E2E All API Plugin' }); + + await manager.getByRole('button', { name: 'Installed' }).click(); + await expect(pluginCard).toBeVisible({ timeout: 10_000 }); + await pluginCard.getByRole('button', { name: 'Select' }).click(); + + await page.getByRole('button', { name: 'Grant all requested' }).click(); + await page.getByRole('button', { name: 'Activate ready plugins' }).click(); + await expect(page.locator('article', { hasText: 'E2E All API Plugin' }).getByText('ready', { exact: true })).toBeVisible({ timeout: 20_000 }); + }); + + await test.step('Verify plugin exercised APIs through logs and extension points', async () => { + const manager = page.getByTestId('plugin-manager'); + + await manager.getByRole('button', { name: 'Logs' }).click(); + await expect(page.getByText('all-api plugin completed')).toBeVisible({ timeout: 20_000 }); + await expect(page.getByText('all-api plugin ready')).toBeVisible({ timeout: 10_000 }); + await manager.getByRole('button', { name: 'Extension points' }).click(); + await expect(page.getByTestId('plugin-extension-counts')).toContainText('Settings pages'); + await expect(page.getByTestId('plugin-extension-counts')).toContainText('Embed renderers'); + await expect(page.getByTestId('plugin-extension-counts')).toContainText('1'); + await expect(page.getByTestId('plugin-conflict-diagnostics')).toContainText( + 'No duplicate route, action, embed, channel, panel, or settings contribution ids detected.' + ); + + await manager.getByRole('button', { exact: true, name: 'Requirements' }).click(); + await expect(page.getByTestId('plugin-server-requirements')).toContainText('E2E All API Plugin'); + await expect(page.getByTestId('plugin-server-requirements')).toContainText('enabled'); + + await manager.getByRole('button', { exact: true, name: 'Settings' }).click(); + await expect(page.getByTestId('plugin-generated-settings')).toContainText('E2E settings contribution'); + await expect(page.getByTestId('plugin-generated-settings')).toContainText('"enabled"'); + + await manager.getByRole('button', { exact: true, name: 'Docs' }).click(); + await expect(page.getByTestId('plugin-installed-docs')).toContainText('Calls every public Toju plugin API surface'); + }); + }); +}); diff --git a/e2e/tests/plugins/plugin-support-api.spec.ts b/e2e/tests/plugins/plugin-support-api.spec.ts new file mode 100644 index 0000000..4167f9f --- /dev/null +++ b/e2e/tests/plugins/plugin-support-api.spec.ts @@ -0,0 +1,369 @@ +import type { APIRequestContext, APIResponse } from '@playwright/test'; +import WebSocket from 'ws'; +import { expect, test } from '../../fixtures/multi-client'; +import { + getPluginApiTestEvent, + readPluginApiTestManifest, + TEST_PLUGIN_ID, + TEST_PLUGIN_P2P_EVENT, + TEST_PLUGIN_RELAY_EVENT +} from '../../helpers/plugin-api-test-fixture'; + +const OWNER_USER_ID = 'plugin-api-owner'; + +interface CreatedServerResponse { + id: string; +} + +interface PluginRequirementResponse { + requirement: { + pluginId: string; + reason?: string; + status: string; + versionRange?: string; + }; +} + +interface PluginEventDefinitionResponse { + eventDefinition: { + direction: string; + eventName: string; + maxPayloadBytes: number; + pluginId: string; + scope: string; + }; +} + +interface PluginSnapshotResponse { + eventDefinitions: PluginEventDefinitionResponse['eventDefinition'][]; + requirements: PluginRequirementResponse['requirement'][]; + serverId: string; +} + +interface SocketMessage { + [key: string]: unknown; + type?: string; +} + +interface TestSocket { + close: () => Promise; + messages: SocketMessage[]; + send: (message: SocketMessage) => void; +} + +test.describe('Plugin support API', () => { + test('covers plugin requirement, event, data, and websocket APIs with the fixture plugin', async ({ request, testServer }) => { + const manifest = await readPluginApiTestManifest(); + const server = await createServer(request, testServer.url, `Plugin API ${Date.now()}`); + const relayEvent = getPluginApiTestEvent(manifest, TEST_PLUGIN_RELAY_EVENT); + const p2pEvent = getPluginApiTestEvent(manifest, TEST_PLUGIN_P2P_EVENT); + const pluginsApi = `${testServer.url}/api/servers/${encodeURIComponent(server.id)}/plugins`; + + await test.step('Initial snapshot is empty', async () => { + const snapshot = await expectJson(await request.get(pluginsApi)); + + expect(snapshot).toEqual(expect.objectContaining({ + eventDefinitions: [], + requirements: [], + serverId: server.id + })); + }); + + await test.step('Requirement API enforces server management permission', async () => { + const response = await request.put(`${pluginsApi}/${TEST_PLUGIN_ID}/requirement`, { + data: { + actorUserId: 'not-the-owner', + status: 'required' + } + }); + const body = await expectJson<{ errorCode: string }>(response, 403); + + expect(body.errorCode).toBe('NOT_AUTHORIZED'); + }); + + await test.step('Requirement and event definition APIs persist the test plugin contract', async () => { + const requirement = await expectJson(await request.put(`${pluginsApi}/${TEST_PLUGIN_ID}/requirement`, { + data: { + actorUserId: OWNER_USER_ID, + reason: manifest.description, + status: 'required', + versionRange: `^${manifest.version}` + } + })); + + expect(requirement.requirement).toEqual(expect.objectContaining({ + pluginId: TEST_PLUGIN_ID, + reason: manifest.description, + status: 'required', + versionRange: `^${manifest.version}` + })); + + const relayDefinition = await upsertEventDefinition(request, pluginsApi, relayEvent); + const p2pDefinition = await upsertEventDefinition(request, pluginsApi, p2pEvent); + + expect(relayDefinition.eventDefinition).toEqual(expect.objectContaining({ + direction: 'serverRelay', + eventName: TEST_PLUGIN_RELAY_EVENT, + pluginId: TEST_PLUGIN_ID, + scope: 'server' + })); + + expect(p2pDefinition.eventDefinition).toEqual(expect.objectContaining({ + direction: 'p2pHint', + eventName: TEST_PLUGIN_P2P_EVENT, + pluginId: TEST_PLUGIN_ID, + scope: 'user' + })); + + const snapshot = await expectJson(await request.get(pluginsApi)); + + expect(snapshot.requirements.map((entry) => entry.pluginId)).toEqual([TEST_PLUGIN_ID]); + expect(snapshot.eventDefinitions.map((entry) => entry.eventName).sort()).toEqual([TEST_PLUGIN_P2P_EVENT, TEST_PLUGIN_RELAY_EVENT]); + }); + + await test.step('Plugin data API refuses arbitrary server persistence', async () => { + const stored = await expectJson<{ errorCode: string }>(await request.put(`${pluginsApi}/${TEST_PLUGIN_ID}/data/settings`, { + data: { + actorUserId: OWNER_USER_ID, + schemaVersion: 1, + scope: 'server', + value: { + enabled: true, + pluginVersion: manifest.version + } + } + }), 410); + + expect(stored.errorCode).toBe('PLUGIN_DATA_DISABLED'); + + const listed = await expectJson<{ errorCode: string }>(await request.get(`${pluginsApi}/${TEST_PLUGIN_ID}/data`, { + params: { + key: 'settings', + scope: 'server', + userId: OWNER_USER_ID + } + }), 410); + + expect(listed.errorCode).toBe('PLUGIN_DATA_DISABLED'); + + const afterDelete = await expectJson<{ errorCode: string }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/data/settings`, { + data: { + actorUserId: OWNER_USER_ID, + scope: 'server' + } + }), 410); + + expect(afterDelete.errorCode).toBe('PLUGIN_DATA_DISABLED'); + }); + + await test.step('WebSocket plugin API sends snapshots, relays server events, and rejects p2p relays', async () => { + const alice = await openTestSocket(testServer.url); + const bob = await openTestSocket(testServer.url); + + try { + alice.send({ type: 'identify', oderId: OWNER_USER_ID, displayName: 'Plugin Owner' }); + bob.send({ type: 'identify', oderId: 'plugin-api-peer', displayName: 'Plugin Peer' }); + alice.send({ type: 'join_server', serverId: server.id }); + bob.send({ type: 'join_server', serverId: server.id }); + + const aliceSnapshot = await waitForSocketMessage(alice, (message) => message.type === 'plugin_requirements'); + const bobSnapshot = await waitForSocketMessage(bob, (message) => message.type === 'plugin_requirements'); + const bobEventNames = (bobSnapshot['snapshot'] as PluginSnapshotResponse).eventDefinitions + .map((entry) => entry.eventName) + .sort(); + + expect((aliceSnapshot['snapshot'] as PluginSnapshotResponse).requirements[0]?.pluginId).toBe(TEST_PLUGIN_ID); + expect(bobEventNames).toEqual([TEST_PLUGIN_P2P_EVENT, TEST_PLUGIN_RELAY_EVENT]); + + alice.send({ + type: 'plugin_event', + eventId: 'relay-event-1', + eventName: TEST_PLUGIN_RELAY_EVENT, + payload: { message: 'hello from fixture plugin' }, + pluginId: TEST_PLUGIN_ID, + serverId: server.id, + sourcePluginUserId: 'fixture-plugin-user' + }); + + const relayedEvent = await waitForSocketMessage(bob, (message) => message.type === 'plugin_event'); + + expect(relayedEvent).toEqual(expect.objectContaining({ + eventId: 'relay-event-1', + eventName: TEST_PLUGIN_RELAY_EVENT, + pluginId: TEST_PLUGIN_ID, + serverId: server.id, + sourcePluginUserId: 'fixture-plugin-user', + sourceUserId: OWNER_USER_ID + })); + + expect(relayedEvent['payload']).toEqual({ message: 'hello from fixture plugin' }); + expect(typeof relayedEvent['emittedAt']).toBe('number'); + + alice.send({ + type: 'plugin_event', + eventId: 'p2p-event-1', + eventName: TEST_PLUGIN_P2P_EVENT, + payload: { hint: true }, + pluginId: TEST_PLUGIN_ID, + serverId: server.id + }); + + const p2pError = await waitForSocketMessage( + alice, + (message) => message.type === 'plugin_error' && message['eventId'] === 'p2p-event-1' + ); + + expect(p2pError['code']).toBe('PLUGIN_EVENT_NOT_RELAYABLE'); + + alice.send({ + type: 'plugin_event', + eventId: 'missing-event-1', + eventName: 'e2e:missing', + payload: {}, + pluginId: TEST_PLUGIN_ID, + serverId: server.id + }); + + const missingError = await waitForSocketMessage( + alice, + (message) => message.type === 'plugin_error' && message['eventId'] === 'missing-event-1' + ); + + expect(missingError['code']).toBe('PLUGIN_EVENT_NOT_REGISTERED'); + } finally { + await Promise.all([alice.close(), bob.close()]); + } + }); + + await test.step('Delete APIs remove event definitions and requirements', async () => { + await expectJson<{ ok: boolean }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/events/${TEST_PLUGIN_RELAY_EVENT}`, { + data: { actorUserId: OWNER_USER_ID } + })); + + await expectJson<{ ok: boolean }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/events/${TEST_PLUGIN_P2P_EVENT}`, { + data: { actorUserId: OWNER_USER_ID } + })); + + await expectJson<{ ok: boolean }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/requirement`, { + data: { actorUserId: OWNER_USER_ID } + })); + + const snapshot = await expectJson(await request.get(pluginsApi)); + + expect(snapshot.eventDefinitions).toEqual([]); + expect(snapshot.requirements).toEqual([]); + }); + }); +}); + +async function createServer( + request: APIRequestContext, + baseUrl: string, + serverName: string +): Promise { + const response = await request.post(`${baseUrl}/api/servers`, { + data: { + channels: [ + { + id: 'general-text', + name: 'general', + position: 0, + type: 'text' + } + ], + description: 'Server for plugin API E2E coverage', + id: `plugin-api-${Date.now()}`, + isPrivate: false, + name: serverName, + ownerId: OWNER_USER_ID, + ownerPublicKey: 'plugin-api-owner-public-key', + tags: ['plugins'] + } + }); + + return await expectJson(response, 201); +} + +async function upsertEventDefinition( + request: APIRequestContext, + pluginsApi: string, + eventDefinition: ReturnType +): Promise { + return await expectJson(await request.put( + `${pluginsApi}/${TEST_PLUGIN_ID}/events/${encodeURIComponent(eventDefinition.eventName)}`, + { + data: { + actorUserId: OWNER_USER_ID, + direction: eventDefinition.direction, + maxPayloadBytes: eventDefinition.maxPayloadBytes, + schemaJson: '{"type":"object"}', + scope: eventDefinition.scope + } + } + )); +} + +async function expectJson(response: APIResponse, status = 200): Promise { + expect(response.status()).toBe(status); + + return await response.json() as T; +} + +async function openTestSocket(baseUrl: string): Promise { + const socketUrl = baseUrl.replace(/^http/, 'ws'); + const socket = new WebSocket(socketUrl); + const messages: SocketMessage[] = []; + + socket.on('message', (data) => { + messages.push(JSON.parse(data.toString()) as SocketMessage); + }); + + await new Promise((resolve, reject) => { + socket.once('open', () => resolve()); + socket.once('error', reject); + }); + + await waitForSocketMessage({ messages, send: () => {}, close: async () => {} }, (message) => message.type === 'connected'); + + return { + close: async () => { + if (socket.readyState === WebSocket.CLOSED) { + return; + } + + await new Promise((resolve) => { + socket.once('close', () => resolve()); + socket.close(); + }); + }, + messages, + send: (message: SocketMessage) => { + socket.send(JSON.stringify(message)); + } + }; +} + +async function waitForSocketMessage( + socket: Pick, + predicate: (message: SocketMessage) => boolean, + timeoutMs = 10_000 +): Promise { + const startedAt = Date.now(); + + return await new Promise((resolve, reject) => { + const interval = setInterval(() => { + const message = socket.messages.find(predicate); + + if (message) { + clearInterval(interval); + resolve(message); + return; + } + + if (Date.now() - startedAt > timeoutMs) { + clearInterval(interval); + reject(new Error('Timed out waiting for websocket message')); + } + }, 25); + }); +} diff --git a/electron/README.md b/electron/README.md index 45b2e5b..30834ab 100644 --- a/electron/README.md +++ b/electron/README.md @@ -28,5 +28,6 @@ Electron main-process package for MetoYou / Toju. This directory owns desktop bo ## Notes - When adding a renderer-facing capability, update the Electron implementation, `preload.ts`, and the renderer bridge in `toju-app/` together. +- Plugin client data is stored in the local Electron SQLite database in the dedicated user-scoped `plugin_data` table. Renderer plugins reach it through CQRS commands/queries exposed by the preload bridge; the signal server must not be used for arbitrary plugin data persistence. - Treat `dist/electron/` and `dist-electron/` as generated output. - See [AGENTS.md](AGENTS.md) for package-level editing rules. diff --git a/electron/api/auth-store.ts b/electron/api/auth-store.ts new file mode 100644 index 0000000..792ed47 --- /dev/null +++ b/electron/api/auth-store.ts @@ -0,0 +1,69 @@ +import { randomBytes } from 'crypto'; + +export interface IssuedToken { + token: string; + userId: string; + username: string; + displayName: string; + signalingServerUrl: string; + issuedAt: number; + expiresAt: number; +} + +const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; +const tokens = new Map(); + +export function issueToken(params: { + userId: string; + username: string; + displayName: string; + signalingServerUrl: string; +}): IssuedToken { + const token = randomBytes(32).toString('hex'); + const issuedAt = Date.now(); + const issued: IssuedToken = { + token, + issuedAt, + expiresAt: issuedAt + TOKEN_TTL_MS, + userId: params.userId, + username: params.username, + displayName: params.displayName, + signalingServerUrl: params.signalingServerUrl + }; + + tokens.set(token, issued); + return issued; +} + +export function consumeToken(token: string): IssuedToken | null { + const issued = tokens.get(token); + + if (!issued) { + return null; + } + + if (issued.expiresAt < Date.now()) { + tokens.delete(token); + return null; + } + + return issued; +} + +export function revokeToken(token: string): void { + tokens.delete(token); +} + +export function clearAllTokens(): void { + tokens.clear(); +} + +export function pruneExpiredTokens(): void { + const now = Date.now(); + + for (const [token, issued] of tokens) { + if (issued.expiresAt < now) { + tokens.delete(token); + } + } +} diff --git a/electron/api/docs-html.ts b/electron/api/docs-html.ts new file mode 100644 index 0000000..405aa96 --- /dev/null +++ b/electron/api/docs-html.ts @@ -0,0 +1,133 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; + +function getScalarBundleCandidates(): string[] { + const processWithResources = process as NodeJS.Process & { resourcesPath?: string }; + const candidates: string[] = []; + + if (processWithResources.resourcesPath) { + candidates.push(path.join(processWithResources.resourcesPath, 'scalar', 'api-reference.js')); + } + + candidates.push(path.join(process.cwd(), 'node_modules', '@scalar', 'api-reference', 'dist', 'browser', 'standalone.js')); + + try { + candidates.push(path.join(path.dirname(require.resolve('@scalar/api-reference')), 'browser', 'standalone.js')); + } catch { + // ignore; the packaged app path above is the production path + } + + return candidates; +} + +export async function getScalarApiReferenceBundlePath(): Promise { + for (const candidate of getScalarBundleCandidates()) { + try { + await fs.access(candidate); + return candidate; + } catch { + // try the next candidate + } + } + + return null; +} + +export function getDocsHtml(specUrl: string): string { + const scalarConfig = { + url: specUrl, + theme: 'default', + layout: 'modern', + proxyUrl: '', + telemetry: false, + persistAuth: false, + showDeveloperTools: 'never', + hideDownloadButton: false, + hideTestRequestButton: false, + hideClientButton: false, + externalUrls: { + dashboardUrl: '', + registryUrl: '', + proxyUrl: '', + apiBaseUrl: '' + }, + agent: { + disabled: true, + hideAddApi: true + }, + mcp: { + disabled: true + } + }; + const contentSecurityPolicy = [ + "default-src 'none'", + "script-src 'self' 'nonce-metoyou-local-api-docs'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: blob:", + "font-src 'self' data:", + "connect-src 'self'", + "base-uri 'none'", + "form-action 'none'", + "frame-ancestors 'none'" + ].join('; '); + + return ` + + + + + + MetoYou Local API + + + +
+ + + + +`; +} diff --git a/electron/api/docusaurus-static.ts b/electron/api/docusaurus-static.ts new file mode 100644 index 0000000..1479237 --- /dev/null +++ b/electron/api/docusaurus-static.ts @@ -0,0 +1,108 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { HttpError } from './http-helpers'; + +const MIME_TYPES_BY_EXTENSION: Record = { + '.css': 'text/css; charset=utf-8', + '.gif': 'image/gif', + '.html': 'text/html; charset=utf-8', + '.ico': 'image/x-icon', + '.jpeg': 'image/jpeg', + '.jpg': 'image/jpeg', + '.js': 'application/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.map': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml; charset=utf-8', + '.txt': 'text/plain; charset=utf-8', + '.webp': 'image/webp', + '.woff': 'font/woff', + '.woff2': 'font/woff2' +}; + +function getDocsRootCandidates(): string[] { + const processWithResources = process as NodeJS.Process & { resourcesPath?: string }; + const candidates: string[] = []; + + if (processWithResources.resourcesPath) { + candidates.push(path.join(processWithResources.resourcesPath, 'docusaurus')); + } + + candidates.push(path.join(process.cwd(), 'docs-site', 'build')); + + return candidates; +} + +async function getDocusaurusBuildRoot(): Promise { + for (const candidate of getDocsRootCandidates()) { + try { + const stat = await fs.stat(candidate); + + if (stat.isDirectory()) { + return candidate; + } + } catch { + // try next candidate + } + } + + return null; +} + +function isPathInside(parent: string, child: string): boolean { + const relative = path.relative(parent, child); + + return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative)); +} + +function resolveAssetPath(root: string, pathname: string): string { + const withoutPrefix = pathname.replace(/^\/docusaurus\/?/u, ''); + const decoded = decodeURIComponent(withoutPrefix); + const normalized = decoded.endsWith('/') || decoded === '' ? path.join(decoded, 'index.html') : decoded; + const absolutePath = path.resolve(root, normalized); + + if (!isPathInside(root, absolutePath)) { + throw new HttpError(400, 'Invalid Docusaurus asset path', 'INVALID_DOCS_PATH'); + } + + return absolutePath; +} + +export async function resolveDocusaurusRoute(pathname: string): Promise<{ filePath: string; contentType: string }> { + const root = await getDocusaurusBuildRoot(); + + if (!root) { + throw new HttpError( + 503, + 'Docusaurus build is not available. Run npm run build:docs before opening the docs endpoint.', + 'DOCUSAURUS_BUILD_MISSING' + ); + } + + let filePath = resolveAssetPath(root, pathname); + + try { + const stat = await fs.stat(filePath); + + if (stat.isDirectory()) { + filePath = path.join(filePath, 'index.html'); + } + } catch { + const directoryIndexPath = path.join(filePath, 'index.html'); + + try { + await fs.access(directoryIndexPath); + filePath = directoryIndexPath; + } catch { + filePath = path.join(root, '404.html'); + } + } + + if (!isPathInside(root, filePath)) { + throw new HttpError(400, 'Invalid Docusaurus asset path', 'INVALID_DOCS_PATH'); + } + + const contentType = MIME_TYPES_BY_EXTENSION[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream'; + + return { filePath, contentType }; +} diff --git a/electron/api/http-helpers.ts b/electron/api/http-helpers.ts new file mode 100644 index 0000000..56c6285 --- /dev/null +++ b/electron/api/http-helpers.ts @@ -0,0 +1,108 @@ +import { IncomingMessage, ServerResponse } from 'http'; + +export interface RequestContext { + method: string; + url: URL; + pathname: string; + headers: IncomingMessage['headers']; + remoteAddress: string; + bearerToken: string | null; +} + +const MAX_BODY_BYTES = 1 * 1024 * 1024; // 1 MiB + +export function getBearerToken(headers: IncomingMessage['headers']): string | null { + const raw = headers.authorization; + + if (typeof raw !== 'string') { + return null; + } + + const trimmed = raw.trim(); + + if (!/^bearer\s+/iu.test(trimmed)) { + return null; + } + + const token = trimmed.replace(/^bearer\s+/iu, '').trim(); + + return token.length > 0 ? token : null; +} + +export async function readJsonBody(req: IncomingMessage): Promise { + const length = Number(req.headers['content-length'] ?? 0); + + if (length > MAX_BODY_BYTES) { + throw new HttpError(413, 'Request body too large', 'BODY_TOO_LARGE'); + } + + const chunks: Buffer[] = []; + + let received = 0; + + for await (const chunk of req) { + const buffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk as string); + + received += buffer.length; + + if (received > MAX_BODY_BYTES) { + throw new HttpError(413, 'Request body too large', 'BODY_TOO_LARGE'); + } + + chunks.push(buffer); + } + + if (chunks.length === 0) { + return {} as T; + } + + const raw = Buffer.concat(chunks).toString('utf8'); + + try { + return JSON.parse(raw) as T; + } catch { + throw new HttpError(400, 'Invalid JSON body', 'INVALID_JSON'); + } +} + +export function sendJson(res: ServerResponse, status: number, payload: unknown): void { + if (!res.headersSent) { + res.statusCode = status; + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Cache-Control', 'no-store'); + } + + res.end(JSON.stringify(payload)); +} + +export function sendText(res: ServerResponse, status: number, text: string, contentType = 'text/plain; charset=utf-8'): void { + if (!res.headersSent) { + res.statusCode = status; + res.setHeader('Content-Type', contentType); + res.setHeader('Cache-Control', 'no-store'); + } + + res.end(text); +} + +export class HttpError extends Error { + readonly status: number; + readonly code: string; + + constructor(status: number, message: string, code: string) { + super(message); + this.status = status; + this.code = code; + } +} + +export function sendError(res: ServerResponse, error: unknown): void { + if (error instanceof HttpError) { + sendJson(res, error.status, { error: error.message, errorCode: error.code }); + return; + } + + const message = error instanceof Error ? error.message : 'Internal server error'; + + sendJson(res, 500, { error: message, errorCode: 'INTERNAL_ERROR' }); +} diff --git a/electron/api/index.ts b/electron/api/index.ts new file mode 100644 index 0000000..24b03bd --- /dev/null +++ b/electron/api/index.ts @@ -0,0 +1,8 @@ +export { + applyLocalApiSettings, + getLocalApiSnapshot, + startLocalApiServer, + stopLocalApiServer, + type LocalApiSnapshot, + type LocalApiStatus +} from './local-api-server'; diff --git a/electron/api/local-api-server.ts b/electron/api/local-api-server.ts new file mode 100644 index 0000000..ab3271e --- /dev/null +++ b/electron/api/local-api-server.ts @@ -0,0 +1,286 @@ +import { + createServer, + IncomingMessage, + Server, + ServerResponse +} from 'http'; +import { createReadStream } from 'fs'; +import { AddressInfo } from 'net'; +import { pipeline } from 'stream/promises'; +import { getDataSource } from '../db/database'; +import { LocalApiSettings, readDesktopSettings } from '../desktop-settings'; +import { authenticate, matchRoute } from './router'; +import { clearAllTokens } from './auth-store'; +import { + HttpError, + RequestContext, + getBearerToken, + readJsonBody, + sendError, + sendJson, + sendText +} from './http-helpers'; + +export type LocalApiStatus = 'stopped' | 'starting' | 'running' | 'error'; + +export interface LocalApiSnapshot { + status: LocalApiStatus; + host: string | null; + port: number | null; + baseUrl: string | null; + error: string | null; + exposeOnLan: boolean; + scalarEnabled: boolean; + docusaurusEnabled: boolean; +} + +let server: Server | null = null; +let currentStatus: LocalApiStatus = 'stopped'; +let currentBindHost: string | null = null; +let currentBindPort: number | null = null; +let currentError: string | null = null; +let activeSettings: LocalApiSettings | null = null; + +function pickBindHost(settings: LocalApiSettings): string { + return settings.exposeOnLan ? '0.0.0.0' : '127.0.0.1'; +} + +function buildBaseUrl(host: string, port: number): string { + const safeHost = host === '0.0.0.0' ? '127.0.0.1' : host; + + return `http://${safeHost}:${port}`; +} + +async function sendFile(res: ServerResponse, status: number, filePath: string, contentType: string): Promise { + if (!res.headersSent) { + res.statusCode = status; + res.setHeader('Content-Type', contentType); + res.setHeader('Cache-Control', 'no-store'); + } + + await pipeline(createReadStream(filePath), res); +} + +export function getLocalApiSnapshot(): LocalApiSnapshot { + const settings = activeSettings ?? readDesktopSettings().localApi; + + return { + status: currentStatus, + host: currentBindHost, + port: currentBindPort, + baseUrl: currentBindHost && currentBindPort ? buildBaseUrl(currentBindHost, currentBindPort) : null, + error: currentError, + exposeOnLan: settings.exposeOnLan, + scalarEnabled: settings.scalarEnabled, + docusaurusEnabled: settings.docusaurusEnabled + }; +} + +async function handleRequest(req: IncomingMessage, res: ServerResponse, settings: LocalApiSettings): Promise { + // CORS for loopback origin only. Local-first; not a public API. + const origin = req.headers.origin; + const allowOrigin = origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/iu.test(origin) ? origin : 'null'; + + res.setHeader('Access-Control-Allow-Origin', allowOrigin); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Max-Age', '600'); + + if (req.method === 'OPTIONS') { + res.statusCode = 204; + res.end(); + return; + } + + let urlObj: URL; + + try { + urlObj = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`); + } catch { + sendJson(res, 400, { error: 'Invalid URL', errorCode: 'INVALID_URL' }); + return; + } + + const requestContext: RequestContext = { + method: (req.method ?? 'GET').toUpperCase(), + url: urlObj, + pathname: urlObj.pathname, + headers: req.headers, + remoteAddress: req.socket.remoteAddress ?? '', + bearerToken: getBearerToken(req.headers) + }; + const { match, methodNotAllowed } = matchRoute(requestContext.method, requestContext.pathname); + + if (!match) { + if (methodNotAllowed) { + sendJson(res, 405, { error: 'Method not allowed', errorCode: 'METHOD_NOT_ALLOWED' }); + } else { + sendJson(res, 404, { error: 'Not found', errorCode: 'NOT_FOUND' }); + } + + return; + } + + if (match.requiresAuth) { + const issued = authenticate(requestContext.bearerToken); + + if (!issued) { + sendJson(res, 401, { error: 'Authentication required', errorCode: 'UNAUTHORIZED' }); + return; + } + } + + const dataSource = getDataSource() ?? null; + + if (match.requiresDatabase && (!dataSource || !dataSource.isInitialized)) { + sendJson(res, 503, { error: 'Database not initialised', errorCode: 'DB_UNAVAILABLE' }); + return; + } + + let bodyCache: unknown | undefined; + + try { + const baseUrl = buildBaseUrl(currentBindHost ?? '127.0.0.1', currentBindPort ?? settings.port); + const result = await match.handler({ + request: requestContext, + settings, + baseUrl, + dataSource, + bodyBuffer: async () => { + if (bodyCache === undefined) { + bodyCache = await readJsonBody(req); + } + + return bodyCache; + } + }); + + if (result.status === 204) { + res.statusCode = 204; + res.end(); + return; + } + + if (result.filePath) { + await sendFile(res, result.status, result.filePath, result.contentType ?? 'application/octet-stream'); + return; + } + + if (result.rawBody !== undefined) { + sendText(res, result.status, result.rawBody, result.contentType ?? 'text/plain; charset=utf-8'); + return; + } + + sendJson(res, result.status, result.body); + } catch (error) { + if (!(error instanceof HttpError)) { + console.error('[LocalApi] Request handler error:', error); + } + + sendError(res, error); + } +} + +export interface StartResult { + ok: boolean; + snapshot: LocalApiSnapshot; +} + +export async function startLocalApiServer(settings: LocalApiSettings): Promise { + if (server) { + await stopLocalApiServer(); + } + + activeSettings = { ...settings, allowedSignalingServers: [...settings.allowedSignalingServers] }; + currentStatus = 'starting'; + currentError = null; + currentBindHost = pickBindHost(settings); + currentBindPort = settings.port; + const httpServer = createServer((req, res) => { + void handleRequest(req, res, activeSettings ?? settings).catch((error) => { + console.error('[LocalApi] Unhandled request error:', error); + + try { + sendError(res, error); + } catch { + // ignore + } + }); + }); + + return await new Promise((resolve) => { + httpServer.once('error', (error) => { + currentStatus = 'error'; + currentError = (error as Error).message; + currentBindPort = null; + server = null; + activeSettings = null; + console.error('[LocalApi] Failed to start:', error); + resolve({ ok: false, snapshot: getLocalApiSnapshot() }); + }); + + httpServer.listen(settings.port, pickBindHost(settings), () => { + const address = httpServer.address() as AddressInfo | null; + + server = httpServer; + currentStatus = 'running'; + currentBindPort = address?.port ?? settings.port; + currentError = null; + console.log(`[LocalApi] Listening on http://${currentBindHost}:${currentBindPort}`); + resolve({ ok: true, snapshot: getLocalApiSnapshot() }); + }); + }); +} + +export async function stopLocalApiServer(): Promise { + const httpServer = server; + + if (!httpServer) { + currentStatus = 'stopped'; + currentBindHost = null; + currentBindPort = null; + activeSettings = null; + return getLocalApiSnapshot(); + } + + await new Promise((resolve) => { + httpServer.close(() => resolve()); + // close() waits for connections; force-close keep-alives so it returns promptly. + httpServer.closeAllConnections?.(); + }); + + server = null; + currentStatus = 'stopped'; + currentBindHost = null; + currentBindPort = null; + currentError = null; + activeSettings = null; + clearAllTokens(); + console.log('[LocalApi] Stopped'); + return getLocalApiSnapshot(); +} + +export async function applyLocalApiSettings(): Promise { + const settings = readDesktopSettings().localApi; + + if (!settings.enabled) { + return await stopLocalApiServer(); + } + + // If already running with the same bind config, no-op (settings like + // scalarEnabled / allowedSignalingServers are read on every request). + if ( + server + && activeSettings + && currentStatus === 'running' + && activeSettings.port === settings.port + && activeSettings.exposeOnLan === settings.exposeOnLan + ) { + activeSettings = { ...settings, allowedSignalingServers: [...settings.allowedSignalingServers] }; + return getLocalApiSnapshot(); + } + + const result = await startLocalApiServer(settings); + + return result.snapshot; +} diff --git a/electron/api/openapi.ts b/electron/api/openapi.ts new file mode 100644 index 0000000..fd45e3f --- /dev/null +++ b/electron/api/openapi.ts @@ -0,0 +1,540 @@ +export interface OpenApiBuildOptions { + baseUrl: string; + appVersion: string; +} + +export function buildOpenApiDocument(options: OpenApiBuildOptions): unknown { + const { baseUrl, appVersion } = options; + const roomIdPathParameter = { name: 'roomId', in: 'path', required: true, schema: { type: 'string' } }; + const userIdPathParameter = { name: 'userId', in: 'path', required: true, schema: { type: 'string' } }; + const messageIdPathParameter = { name: 'messageId', in: 'path', required: true, schema: { type: 'string' } }; + const sinceTimestampQueryParameter = { + name: 'sinceTimestamp', + in: 'query', + required: true, + schema: { type: 'integer', minimum: 0, format: 'int64' } + }; + + return { + openapi: '3.1.0', + info: { + title: 'MetoYou Local Desktop API', + version: appVersion, + description: + 'Authenticated local HTTP API exposed by the MetoYou desktop app. ' + + 'Authentication is performed against a configured signaling server. ' + + 'Bearer tokens issued here are scoped to this device only.' + }, + servers: [{ url: baseUrl }], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'opaque' + } + }, + schemas: { + Error: { + type: 'object', + required: ['error'], + properties: { + error: { type: 'string' }, + errorCode: { type: 'string' } + } + }, + LoginRequest: { + type: 'object', + required: [ + 'username', + 'password', + 'serverUrl' + ], + properties: { + username: { type: 'string' }, + password: { type: 'string' }, + serverUrl: { + type: 'string', + format: 'uri', + description: 'Base URL of the signaling server to authenticate against. Must be in the allowed list configured in the desktop app.' + } + } + }, + LoginResponse: { + type: 'object', + required: [ + 'token', + 'expiresAt', + 'user' + ], + properties: { + token: { type: 'string' }, + expiresAt: { type: 'integer', format: 'int64' }, + user: { $ref: '#/components/schemas/AuthUser' } + } + }, + AuthUser: { + type: 'object', + required: [ + 'id', + 'username', + 'displayName' + ], + properties: { + id: { type: 'string' }, + username: { type: 'string' }, + displayName: { type: 'string' } + } + }, + Profile: { + type: 'object', + properties: { + id: { type: 'string' }, + username: { type: 'string' }, + displayName: { type: 'string' }, + description: { type: 'string' }, + avatarUrl: { type: 'string' }, + status: { type: 'string' } + } + }, + Room: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' } + }, + additionalProperties: true + }, + User: { + type: 'object', + properties: { + id: { type: 'string' }, + oderId: { type: 'string' }, + username: { type: 'string' }, + displayName: { type: 'string' }, + status: { type: 'string' }, + role: { type: 'string' }, + isOnline: { type: 'boolean' } + }, + additionalProperties: true + }, + Message: { + type: 'object', + properties: { + id: { type: 'string' }, + roomId: { type: 'string' }, + channelId: { type: 'string' }, + senderId: { type: 'string' }, + senderName: { type: 'string' }, + content: { type: 'string' }, + timestamp: { type: 'integer', format: 'int64' }, + editedAt: { type: 'integer', format: 'int64' }, + isDeleted: { type: 'boolean' } + }, + additionalProperties: true + }, + Reaction: { + type: 'object', + properties: { + id: { type: 'string' }, + messageId: { type: 'string' }, + userId: { type: 'string' }, + oderId: { type: 'string' }, + emoji: { type: 'string' }, + timestamp: { type: 'integer', format: 'int64' } + }, + additionalProperties: true + }, + Attachment: { + type: 'object', + properties: { + id: { type: 'string' }, + messageId: { type: 'string' }, + filename: { type: 'string' }, + size: { type: 'integer' }, + mime: { type: 'string' }, + isImage: { type: 'boolean' }, + filePath: { type: 'string' }, + savedPath: { type: 'string' } + }, + additionalProperties: true + }, + Ban: { + type: 'object', + properties: { + oderId: { type: 'string' }, + roomId: { type: 'string' }, + userId: { type: 'string' }, + bannedBy: { type: 'string' }, + displayName: { type: 'string' }, + reason: { type: 'string' }, + expiresAt: { type: 'integer', format: 'int64' }, + timestamp: { type: 'integer', format: 'int64' } + }, + additionalProperties: true + }, + PluginDataValue: { + type: 'object', + properties: { + value: {} + } + }, + MetaValue: { + type: 'object', + properties: { + key: { type: 'string' }, + value: { type: ['string', 'null'] } + } + } + } + }, + security: [{ bearerAuth: [] }], + paths: { + '/api/health': { + get: { + security: [], + summary: 'Liveness probe', + responses: { + '200': { + description: 'Service is alive', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + status: { type: 'string' }, + version: { type: 'string' } + } + } + } + } + } + } + } + }, + '/api/openapi.json': { + get: { + security: [], + summary: 'OpenAPI specification', + responses: { '200': { description: 'This document' } } + } + }, + '/api/auth/login': { + post: { + security: [], + summary: 'Exchange username/password (validated by a signaling server) for a bearer token', + requestBody: { + required: true, + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/LoginRequest' } + } + } + }, + responses: { + '200': { + description: 'Token issued', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/LoginResponse' } + } + } + }, + '401': { + description: 'Invalid credentials', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Error' } } + } + }, + '403': { + description: 'Signaling server URL not allowed', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Error' } } + } + } + } + } + }, + '/api/auth/logout': { + post: { + summary: 'Revoke the current bearer token', + responses: { '204': { description: 'Token revoked' } } + } + }, + '/api/profile': { + get: { + summary: 'Get the current user profile', + responses: { + '200': { + description: 'Current user profile', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Profile' } + } + } + }, + '404': { + description: 'No current user is set on this device', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Error' } } + } + } + } + } + }, + '/api/rooms': { + get: { + summary: 'List rooms (servers) known to this device', + responses: { + '200': { + description: 'Rooms array', + content: { + 'application/json': { + schema: { + type: 'array', + items: { $ref: '#/components/schemas/Room' } + } + } + } + } + } + } + }, + '/api/rooms/{roomId}': { + get: { + summary: 'Get a room by id', + parameters: [roomIdPathParameter], + responses: { + '200': { + description: 'Room details', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Room' } + } + } + }, + '404': { + description: 'Room not found', + content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } + } + } + } + }, + '/api/rooms/{roomId}/users': { + get: { + summary: 'List users known for a room', + parameters: [roomIdPathParameter], + responses: { + '200': { + description: 'Users array', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/User' } } + } + } + } + } + } + }, + '/api/rooms/{roomId}/messages': { + get: { + summary: 'List messages for a room', + parameters: [ + roomIdPathParameter, + { name: 'limit', in: 'query', required: false, schema: { type: 'integer', minimum: 1, maximum: 500 } }, + { name: 'offset', in: 'query', required: false, schema: { type: 'integer', minimum: 0 } } + ], + responses: { + '200': { + description: 'Messages array', + content: { + 'application/json': { + schema: { + type: 'array', + items: { $ref: '#/components/schemas/Message' } + } + } + } + } + } + } + }, + '/api/rooms/{roomId}/messages/since': { + get: { + summary: 'List room messages after a timestamp', + parameters: [roomIdPathParameter, sinceTimestampQueryParameter], + responses: { + '200': { + description: 'Messages array', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Message' } } + } + } + } + } + } + }, + '/api/rooms/{roomId}/bans': { + get: { + summary: 'List active bans for a room', + parameters: [roomIdPathParameter], + responses: { + '200': { + description: 'Bans array', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Ban' } } + } + } + } + } + } + }, + '/api/rooms/{roomId}/bans/{userId}': { + get: { + summary: 'Check whether a user is banned in a room', + parameters: [roomIdPathParameter, userIdPathParameter], + responses: { + '200': { + description: 'Ban status', + content: { + 'application/json': { + schema: { + type: 'object', + required: ['isBanned'], + properties: { isBanned: { type: 'boolean' } } + } + } + } + } + } + } + }, + '/api/messages/{messageId}': { + get: { + summary: 'Get a message by id', + parameters: [messageIdPathParameter], + responses: { + '200': { + description: 'Message details', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Message' } + } + } + }, + '404': { + description: 'Message not found', + content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } + } + } + } + }, + '/api/messages/{messageId}/reactions': { + get: { + summary: 'List reactions for a message', + parameters: [messageIdPathParameter], + responses: { + '200': { + description: 'Reactions array', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Reaction' } } + } + } + } + } + } + }, + '/api/messages/{messageId}/attachments': { + get: { + summary: 'List attachments for a message', + parameters: [messageIdPathParameter], + responses: { + '200': { + description: 'Attachments array', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Attachment' } } + } + } + } + } + } + }, + '/api/users/{userId}': { + get: { + summary: 'Get a user by id', + parameters: [userIdPathParameter], + responses: { + '200': { + description: 'User details', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/User' } + } + } + }, + '404': { + description: 'User not found', + content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } + } + } + } + }, + '/api/attachments': { + get: { + summary: 'List all attachments stored on this device', + responses: { + '200': { + description: 'Attachments array', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Attachment' } } + } + } + } + } + } + }, + '/api/plugin-data': { + get: { + summary: 'Read a plugin data value', + parameters: [ + { name: 'pluginId', in: 'query', required: true, schema: { type: 'string' } }, + { name: 'key', in: 'query', required: true, schema: { type: 'string' } }, + { name: 'scope', in: 'query', required: true, schema: { type: 'string', enum: ['local', 'server'] } }, + { name: 'serverId', in: 'query', required: false, schema: { type: 'string' } } + ], + responses: { + '200': { + description: 'Plugin data value', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/PluginDataValue' } + } + } + } + } + } + }, + '/api/meta/{key}': { + get: { + summary: 'Read a desktop metadata value', + parameters: [{ name: 'key', in: 'path', required: true, schema: { type: 'string' } }], + responses: { + '200': { + description: 'Metadata value', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/MetaValue' } + } + } + } + } + } + } + } + }; +} diff --git a/electron/api/router.ts b/electron/api/router.ts new file mode 100644 index 0000000..8440094 --- /dev/null +++ b/electron/api/router.ts @@ -0,0 +1,511 @@ +import { app, net } from 'electron'; +import { DataSource } from 'typeorm'; +import { buildQueryHandlers } from '../cqrs/queries'; +import { + QueryType, + QueryTypeKey, + Query +} from '../cqrs/types'; +import { + issueToken, + consumeToken, + revokeToken, + IssuedToken +} from './auth-store'; +import { buildOpenApiDocument } from './openapi'; +import { HttpError, RequestContext } from './http-helpers'; +import { getDocsHtml, getScalarApiReferenceBundlePath } from './docs-html'; +import { resolveDocusaurusRoute } from './docusaurus-static'; +import { LocalApiSettings } from '../desktop-settings'; + +export interface RouteResponse { + status: number; + body: unknown; + contentType?: string; + filePath?: string; + rawBody?: string; +} + +export interface RouteContext { + request: RequestContext; + settings: LocalApiSettings; + baseUrl: string; + dataSource: DataSource | null; + bodyBuffer: () => Promise; +} + +type RouteHandler = (context: RouteContext) => Promise; + +interface RouteMatch { + handler: RouteHandler; + params: Record; + requiresAuth: boolean; + requiresDatabase: boolean; +} + +interface RouteDefinition { + method: string; + pattern: RegExp; + paramKeys: string[]; + handler: RouteHandler; + requiresAuth: boolean; + requiresDatabase: boolean; +} + +function compilePattern(template: string): { pattern: RegExp; paramKeys: string[] } { + const paramKeys: string[] = []; + const escaped = template.replace(/[.*+?^${}()|[\]\\]/g, (match) => { + if (match === '*' || match === '+' || match === '?') + return `\\${match}`; + + return `\\${match}`; + }); + const source = template.replace(/\{([^}]+)\}/g, (_full, key: string) => { + paramKeys.push(key); + return '([^/]+)'; + }); + + void escaped; + + return { pattern: new RegExp(`^${source}$`), paramKeys }; +} + +function defineRoute(method: string, template: string, handler: RouteHandler, requiresAuth: boolean, requiresDatabase = true): RouteDefinition { + const compiled = compilePattern(template); + + return { method: method.toUpperCase(), pattern: compiled.pattern, paramKeys: compiled.paramKeys, handler, requiresAuth, requiresDatabase }; +} + +function runQuery(dataSource: DataSource, query: Query): Promise { + const handlers = buildQueryHandlers(dataSource) as Record Promise>; + const handler = handlers[query.type as QueryTypeKey]; + + if (!handler) { + throw new HttpError(500, `No handler registered for query: ${query.type}`, 'UNKNOWN_QUERY'); + } + + return handler(query) as Promise; +} + +function requireDataSource(dataSource: DataSource | null): DataSource { + if (!dataSource) { + throw new HttpError(503, 'Database not initialised', 'DB_UNAVAILABLE'); + } + + return dataSource; +} + +function clampInt(value: unknown, min: number, max: number, fallback: number): number { + const parsed = typeof value === 'string' ? Number(value) : NaN; + + if (!Number.isFinite(parsed)) + return fallback; + + return Math.max(min, Math.min(max, Math.floor(parsed))); +} + +function getTrailingPathParam(pathname: string, pattern: RegExp, name: string): string { + const value = pattern.exec(pathname)?.[1]; + + if (!value) { + throw new HttpError(400, `${name} is required`, 'INVALID_REQUEST'); + } + + return decodeURIComponent(value); +} + +function getRequiredQueryParam(ctx: RouteContext, name: string): string { + const value = ctx.request.url.searchParams.get(name)?.trim() ?? ''; + + if (!value) { + throw new HttpError(400, `${name} is required`, 'INVALID_REQUEST'); + } + + return value; +} + +function getRequiredTimestamp(ctx: RouteContext, name: string): number { + const raw = getRequiredQueryParam(ctx, name); + const value = Number(raw); + + if (!Number.isFinite(value) || value < 0) { + throw new HttpError(400, `${name} must be a non-negative timestamp`, 'INVALID_REQUEST'); + } + + return Math.floor(value); +} + +const ROUTES: RouteDefinition[] = [ + defineRoute('GET', '/api/health', async (ctx): Promise => ({ + status: 200, + body: { status: 'ok', version: app.getVersion(), timestamp: Date.now(), exposeOnLan: ctx.settings.exposeOnLan } + }), false, false), + + defineRoute('GET', '/api/openapi.json', async (ctx): Promise => ({ + status: 200, + body: buildOpenApiDocument({ baseUrl: ctx.baseUrl, appVersion: app.getVersion() }) + }), false, false), + + defineRoute('GET', '/docs', async (ctx): Promise => { + if (!ctx.settings.scalarEnabled) { + return { + status: 404, + body: null, + contentType: 'text/plain; charset=utf-8', + rawBody: 'API documentation is disabled. Enable Scalar in desktop settings to view it.' + }; + } + + return { + status: 200, + body: null, + contentType: 'text/html; charset=utf-8', + rawBody: getDocsHtml(`${ctx.baseUrl}/api/openapi.json`) + }; + }, false, false), + + defineRoute('GET', '/docusaurus(?:/.*)?', async (ctx): Promise => { + if (!ctx.settings.docusaurusEnabled) { + return { + status: 404, + body: null, + contentType: 'text/plain; charset=utf-8', + rawBody: 'Docusaurus documentation is disabled. Open documentation from the desktop app to activate it.' + }; + } + + const docsRoute = await resolveDocusaurusRoute(ctx.request.pathname); + + return { + status: 200, + body: null, + contentType: docsRoute.contentType, + filePath: docsRoute.filePath + }; + }, false, false), + + defineRoute('GET', '/scalar/api-reference.js', async (ctx): Promise => { + if (!ctx.settings.scalarEnabled) { + return { + status: 404, + body: null, + contentType: 'text/plain; charset=utf-8', + rawBody: 'API documentation is disabled. Enable Scalar in desktop settings to view it.' + }; + } + + const bundlePath = await getScalarApiReferenceBundlePath(); + + if (!bundlePath) { + throw new HttpError(503, 'Scalar API reference bundle is not available in this build', 'SCALAR_BUNDLE_MISSING'); + } + + return { + status: 200, + body: null, + contentType: 'application/javascript; charset=utf-8', + filePath: bundlePath + }; + }, false, false), + + defineRoute('POST', '/api/auth/login', async (ctx): Promise => { + const body = await ctx.bodyBuffer() as { username?: unknown; password?: unknown; serverUrl?: unknown }; + const username = typeof body.username === 'string' ? body.username.trim() : ''; + const password = typeof body.password === 'string' ? body.password : ''; + const serverUrl = typeof body.serverUrl === 'string' ? body.serverUrl.trim().replace(/\/+$/u, '') : ''; + + if (!username || !password || !serverUrl) { + throw new HttpError(400, 'username, password, and serverUrl are required', 'INVALID_REQUEST'); + } + + if (!/^https?:\/\//iu.test(serverUrl)) { + throw new HttpError(400, 'serverUrl must be an http or https URL', 'INVALID_REQUEST'); + } + + if (ctx.settings.allowedSignalingServers.length === 0) { + throw new HttpError(403, 'No signaling servers are allowed for local API authentication. Add one in desktop settings.', 'NO_ALLOWED_SERVERS'); + } + + if (!ctx.settings.allowedSignalingServers.includes(serverUrl)) { + throw new HttpError(403, 'Signaling server URL is not in the allowed list', 'SERVER_NOT_ALLOWED'); + } + + let response: Response; + + try { + response = await net.fetch(`${serverUrl}/api/users/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + } catch (error) { + throw new HttpError(502, `Failed to reach signaling server: ${(error as Error).message}`, 'UPSTREAM_UNREACHABLE'); + } + + if (response.status === 401 || response.status === 403) { + throw new HttpError(401, 'Invalid credentials', 'INVALID_CREDENTIALS'); + } + + if (!response.ok) { + throw new HttpError(502, `Signaling server rejected login (${response.status})`, 'UPSTREAM_ERROR'); + } + + const remote = await response.json() as { id?: string; username?: string; displayName?: string }; + + if (!remote.id || !remote.username) { + throw new HttpError(502, 'Signaling server returned an unexpected response', 'UPSTREAM_BAD_RESPONSE'); + } + + const issued = issueToken({ + userId: remote.id, + username: remote.username, + displayName: remote.displayName ?? remote.username, + signalingServerUrl: serverUrl + }); + + return { + status: 200, + body: { + token: issued.token, + expiresAt: issued.expiresAt, + user: { + id: issued.userId, + username: issued.username, + displayName: issued.displayName + } + } + }; + }, false), + + defineRoute('POST', '/api/auth/logout', async (ctx): Promise => { + if (ctx.request.bearerToken) { + revokeToken(ctx.request.bearerToken); + } + + return { status: 204, body: null }; + }, true), + + defineRoute('GET', '/api/profile', async (ctx): Promise => { + const user = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetCurrentUser, + payload: {} + }); + + if (!user) { + throw new HttpError(404, 'No current user is set on this device', 'NO_CURRENT_USER'); + } + + return { status: 200, body: user }; + }, true), + + defineRoute('GET', '/api/rooms', async (ctx): Promise => { + const rooms = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetAllRooms, + payload: {} + }); + + return { status: 200, body: rooms ?? [] }; + }, true), + + defineRoute('GET', '/api/rooms/{roomId}', async (ctx): Promise => { + const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)$/u, 'roomId'); + const room = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetRoom, + payload: { roomId } + }); + + if (!room) { + throw new HttpError(404, 'Room not found on this device', 'ROOM_NOT_FOUND'); + } + + return { status: 200, body: room }; + }, true), + + defineRoute('GET', '/api/rooms/{roomId}/users', async (ctx): Promise => { + const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/users$/u, 'roomId'); + const users = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetUsersByRoom, + payload: { roomId } + }); + + return { status: 200, body: users ?? [] }; + }, true), + + defineRoute('GET', '/api/rooms/{roomId}/messages', async (ctx): Promise => { + const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/messages$/u, 'roomId'); + const limit = clampInt(ctx.request.url.searchParams.get('limit'), 1, 500, 100); + const offset = clampInt(ctx.request.url.searchParams.get('offset'), 0, Number.MAX_SAFE_INTEGER, 0); + const messages = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetMessages, + payload: { roomId, limit, offset } + }); + + return { status: 200, body: messages ?? [] }; + }, true), + + defineRoute('GET', '/api/rooms/{roomId}/messages/since', async (ctx): Promise => { + const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/messages\/since$/u, 'roomId'); + const sinceTimestamp = getRequiredTimestamp(ctx, 'sinceTimestamp'); + const messages = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetMessagesSince, + payload: { roomId, sinceTimestamp } + }); + + return { status: 200, body: messages ?? [] }; + }, true), + + defineRoute('GET', '/api/rooms/{roomId}/bans', async (ctx): Promise => { + const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/bans$/u, 'roomId'); + const bans = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetBansForRoom, + payload: { roomId } + }); + + return { status: 200, body: bans ?? [] }; + }, true), + + defineRoute('GET', '/api/rooms/{roomId}/bans/{userId}', async (ctx): Promise => { + const match = /\/api\/rooms\/([^/]+)\/bans\/([^/]+)$/u.exec(ctx.request.pathname); + + if (!match) { + throw new HttpError(400, 'roomId and userId are required', 'INVALID_REQUEST'); + } + + const isBanned = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.IsUserBanned, + payload: { roomId: decodeURIComponent(match[1]), userId: decodeURIComponent(match[2]) } + }); + + return { status: 200, body: { isBanned } }; + }, true), + + defineRoute('GET', '/api/messages/{messageId}', async (ctx): Promise => { + const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)$/u, 'messageId'); + const message = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetMessageById, + payload: { messageId } + }); + + if (!message) { + throw new HttpError(404, 'Message not found on this device', 'MESSAGE_NOT_FOUND'); + } + + return { status: 200, body: message }; + }, true), + + defineRoute('GET', '/api/messages/{messageId}/reactions', async (ctx): Promise => { + const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)\/reactions$/u, 'messageId'); + const reactions = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetReactionsForMessage, + payload: { messageId } + }); + + return { status: 200, body: reactions ?? [] }; + }, true), + + defineRoute('GET', '/api/messages/{messageId}/attachments', async (ctx): Promise => { + const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)\/attachments$/u, 'messageId'); + const attachments = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetAttachmentsForMessage, + payload: { messageId } + }); + + return { status: 200, body: attachments ?? [] }; + }, true), + + defineRoute('GET', '/api/users/{userId}', async (ctx): Promise => { + const userId = getTrailingPathParam(ctx.request.pathname, /\/api\/users\/([^/]+)$/u, 'userId'); + const user = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetUser, + payload: { userId } + }); + + if (!user) { + throw new HttpError(404, 'User not found on this device', 'USER_NOT_FOUND'); + } + + return { status: 200, body: user }; + }, true), + + defineRoute('GET', '/api/attachments', async (ctx): Promise => { + const attachments = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetAllAttachments, + payload: {} + }); + + return { status: 200, body: attachments ?? [] }; + }, true), + + defineRoute('GET', '/api/plugin-data', async (ctx): Promise => { + const pluginId = getRequiredQueryParam(ctx, 'pluginId'); + const key = getRequiredQueryParam(ctx, 'key'); + const scope = getRequiredQueryParam(ctx, 'scope'); + + if (scope !== 'local' && scope !== 'server') { + throw new HttpError(400, 'scope must be local or server', 'INVALID_REQUEST'); + } + + const value = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetPluginData, + payload: { + key, + pluginId, + scope, + serverId: ctx.request.url.searchParams.get('serverId') ?? undefined + } + }); + + return { status: 200, body: { value } }; + }, true), + + defineRoute('GET', '/api/meta/{key}', async (ctx): Promise => { + const key = getTrailingPathParam(ctx.request.pathname, /\/api\/meta\/([^/]+)$/u, 'key'); + const value = await runQuery(requireDataSource(ctx.dataSource), { + type: QueryType.GetMeta, + payload: { key } + }); + + return { status: 200, body: { key, value } }; + }, true) +]; + +export interface RoutingResult { + match: RouteMatch | null; + methodNotAllowed: boolean; +} + +export function matchRoute(method: string, pathname: string): RoutingResult { + let methodNotAllowed = false; + + for (const route of ROUTES) { + const result = route.pattern.exec(pathname); + + if (!result) + continue; + + if (route.method !== method) { + methodNotAllowed = true; + continue; + } + + const params: Record = {}; + + for (let index = 0; index < route.paramKeys.length; index++) { + params[route.paramKeys[index]] = result[index + 1]; + } + + return { + match: { handler: route.handler, params, requiresAuth: route.requiresAuth, requiresDatabase: route.requiresDatabase }, + methodNotAllowed: false + }; + } + + return { match: null, methodNotAllowed }; +} + +export function authenticate(token: string | null): IssuedToken | null { + if (!token) + return null; + + return consumeToken(token); +} diff --git a/electron/app/lifecycle.ts b/electron/app/lifecycle.ts index 8621657..2202308 100644 --- a/electron/app/lifecycle.ts +++ b/electron/app/lifecycle.ts @@ -2,6 +2,7 @@ import { app, BrowserWindow } from 'electron'; import { cleanupLinuxScreenShareAudioRouting } from '../audio/linux-screen-share-routing'; import { initializeDesktopUpdater, shutdownDesktopUpdater } from '../update/desktop-updater'; import { synchronizeAutoStartSetting } from './auto-start'; +import { applyLocalApiSettings, stopLocalApiServer } from '../api'; import { initializeDatabase, destroyDatabase, @@ -21,6 +22,14 @@ import { } from '../ipc'; import { startIdleMonitor, stopIdleMonitor } from '../idle/idle-monitor'; +function startLocalApiAfterWindowReady(): void { + setImmediate(() => { + void applyLocalApiSettings().catch((error: unknown) => { + console.error('[LocalApi] Failed to apply settings after window startup:', error); + }); + }); +} + export function registerAppLifecycle(): void { app.whenReady().then(async () => { const dockIconPath = getDockIconPath(); @@ -35,6 +44,7 @@ export function registerAppLifecycle(): void { await synchronizeAutoStartSetting(); initializeDesktopUpdater(); await createWindow(); + startLocalApiAfterWindowReady(); startIdleMonitor(); app.on('activate', () => { @@ -60,6 +70,7 @@ export function registerAppLifecycle(): void { event.preventDefault(); shutdownDesktopUpdater(); stopIdleMonitor(); + await stopLocalApiServer(); await cleanupLinuxScreenShareAudioRouting(); await destroyDatabase(); app.quit(); diff --git a/electron/cqrs/commands/handlers/clearAllData.ts b/electron/cqrs/commands/handlers/clearAllData.ts index a993493..7c5c520 100644 --- a/electron/cqrs/commands/handlers/clearAllData.ts +++ b/electron/cqrs/commands/handlers/clearAllData.ts @@ -11,7 +11,8 @@ import { ReactionEntity, BanEntity, AttachmentEntity, - MetaEntity + MetaEntity, + PluginDataEntity } from '../../../entities'; export async function handleClearAllData(dataSource: DataSource): Promise { @@ -27,4 +28,5 @@ export async function handleClearAllData(dataSource: DataSource): Promise await dataSource.getRepository(BanEntity).clear(); await dataSource.getRepository(AttachmentEntity).clear(); await dataSource.getRepository(MetaEntity).clear(); + await dataSource.getRepository(PluginDataEntity).clear(); } diff --git a/electron/cqrs/commands/handlers/clearRoomMessages.ts b/electron/cqrs/commands/handlers/clearRoomMessages.ts index 475f729..6ba682e 100644 --- a/electron/cqrs/commands/handlers/clearRoomMessages.ts +++ b/electron/cqrs/commands/handlers/clearRoomMessages.ts @@ -1,9 +1,15 @@ import { DataSource } from 'typeorm'; import { MessageEntity } from '../../../entities'; import { ClearRoomMessagesCommand } from '../../types'; +import { getCurrentUserScope } from '../../current-user-scope'; export async function handleClearRoomMessages(command: ClearRoomMessagesCommand, dataSource: DataSource): Promise { const repo = dataSource.getRepository(MessageEntity); + const currentUserId = await getCurrentUserScope(dataSource); - await repo.delete({ roomId: command.payload.roomId }); + if (!currentUserId) { + return; + } + + await repo.delete({ roomId: command.payload.roomId, ownerUserId: currentUserId }); } diff --git a/electron/cqrs/commands/handlers/deleteMessage.ts b/electron/cqrs/commands/handlers/deleteMessage.ts index 3188d64..25a49ff 100644 --- a/electron/cqrs/commands/handlers/deleteMessage.ts +++ b/electron/cqrs/commands/handlers/deleteMessage.ts @@ -1,9 +1,15 @@ import { DataSource } from 'typeorm'; import { MessageEntity } from '../../../entities'; import { DeleteMessageCommand } from '../../types'; +import { getCurrentUserScope } from '../../current-user-scope'; export async function handleDeleteMessage(command: DeleteMessageCommand, dataSource: DataSource): Promise { const repo = dataSource.getRepository(MessageEntity); + const currentUserId = await getCurrentUserScope(dataSource); - await repo.delete({ id: command.payload.messageId }); + if (!currentUserId) { + return; + } + + await repo.delete({ id: command.payload.messageId, ownerUserId: currentUserId }); } diff --git a/electron/cqrs/commands/handlers/deletePluginData.ts b/electron/cqrs/commands/handlers/deletePluginData.ts new file mode 100644 index 0000000..dac49f8 --- /dev/null +++ b/electron/cqrs/commands/handlers/deletePluginData.ts @@ -0,0 +1,21 @@ +import { DataSource } from 'typeorm'; +import { getCurrentUserScope } from '../../current-user-scope'; +import { PluginDataEntity } from '../../../entities'; +import { DeletePluginDataCommand } from '../../types'; + +export async function handleDeletePluginData(command: DeletePluginDataCommand, dataSource: DataSource): Promise { + const { payload } = command; + const ownerUserId = await getCurrentUserScope(dataSource); + + if (!ownerUserId) { + return; + } + + await dataSource.getRepository(PluginDataEntity).delete({ + key: payload.key, + ownerUserId, + pluginId: payload.pluginId, + scope: payload.scope, + serverId: payload.serverId ?? '' + }); +} diff --git a/electron/cqrs/commands/handlers/deleteRoom.ts b/electron/cqrs/commands/handlers/deleteRoom.ts index a4b339c..35e34b7 100644 --- a/electron/cqrs/commands/handlers/deleteRoom.ts +++ b/electron/cqrs/commands/handlers/deleteRoom.ts @@ -3,23 +3,39 @@ import { RoomChannelPermissionEntity, RoomChannelEntity, RoomEntity, + RoomOwnerEntity, RoomMemberEntity, RoomRoleEntity, RoomUserRoleEntity, MessageEntity } from '../../../entities'; import { DeleteRoomCommand } from '../../types'; +import { getCurrentUserScope } from '../../current-user-scope'; export async function handleDeleteRoom(command: DeleteRoomCommand, dataSource: DataSource): Promise { const { roomId } = command.payload; await dataSource.transaction(async (manager) => { + const currentUserId = await getCurrentUserScope(manager); + + if (!currentUserId) { + return; + } + + await manager.getRepository(RoomOwnerEntity).delete({ roomId, userId: currentUserId }); + await manager.getRepository(MessageEntity).delete({ roomId, ownerUserId: currentUserId }); + + const remainingOwners = await manager.getRepository(RoomOwnerEntity).count({ where: { roomId } }); + + if (remainingOwners > 0) { + return; + } + await manager.getRepository(RoomChannelPermissionEntity).delete({ roomId }); await manager.getRepository(RoomChannelEntity).delete({ roomId }); await manager.getRepository(RoomMemberEntity).delete({ roomId }); await manager.getRepository(RoomRoleEntity).delete({ roomId }); await manager.getRepository(RoomUserRoleEntity).delete({ roomId }); await manager.getRepository(RoomEntity).delete({ id: roomId }); - await manager.getRepository(MessageEntity).delete({ roomId }); }); } diff --git a/electron/cqrs/commands/handlers/saveMessage.ts b/electron/cqrs/commands/handlers/saveMessage.ts index 75dcd88..d4f87b0 100644 --- a/electron/cqrs/commands/handlers/saveMessage.ts +++ b/electron/cqrs/commands/handlers/saveMessage.ts @@ -2,15 +2,18 @@ import { DataSource } from 'typeorm'; import { MessageEntity } from '../../../entities'; import { replaceMessageReactions } from '../../relations'; import { SaveMessageCommand } from '../../types'; +import { getCurrentUserScope } from '../../current-user-scope'; export async function handleSaveMessage(command: SaveMessageCommand, dataSource: DataSource): Promise { const { message } = command.payload; await dataSource.transaction(async (manager) => { + const currentUserId = await getCurrentUserScope(manager); const repo = manager.getRepository(MessageEntity); const entity = repo.create({ id: message.id, roomId: message.roomId, + ownerUserId: currentUserId, channelId: message.channelId ?? null, senderId: message.senderId, senderName: message.senderName, diff --git a/electron/cqrs/commands/handlers/saveMeta.ts b/electron/cqrs/commands/handlers/saveMeta.ts new file mode 100644 index 0000000..ec23090 --- /dev/null +++ b/electron/cqrs/commands/handlers/saveMeta.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { MetaEntity } from '../../../entities'; +import { SaveMetaCommand } from '../../types'; + +export async function handleSaveMeta(command: SaveMetaCommand, dataSource: DataSource): Promise { + await dataSource.getRepository(MetaEntity).save({ + key: command.payload.key, + value: command.payload.value + }); +} diff --git a/electron/cqrs/commands/handlers/savePluginData.ts b/electron/cqrs/commands/handlers/savePluginData.ts new file mode 100644 index 0000000..be9c268 --- /dev/null +++ b/electron/cqrs/commands/handlers/savePluginData.ts @@ -0,0 +1,23 @@ +import { DataSource } from 'typeorm'; +import { getCurrentUserScope } from '../../current-user-scope'; +import { PluginDataEntity } from '../../../entities'; +import { SavePluginDataCommand } from '../../types'; + +export async function handleSavePluginData(command: SavePluginDataCommand, dataSource: DataSource): Promise { + const { payload } = command; + const ownerUserId = await getCurrentUserScope(dataSource); + + if (!ownerUserId) { + return; + } + + await dataSource.getRepository(PluginDataEntity).save({ + key: payload.key, + ownerUserId, + pluginId: payload.pluginId, + scope: payload.scope, + serverId: payload.serverId ?? '', + updatedAt: Date.now(), + valueJson: JSON.stringify(payload.value ?? null) + }); +} diff --git a/electron/cqrs/commands/handlers/saveRoom.ts b/electron/cqrs/commands/handlers/saveRoom.ts index b4b47a9..f5f576a 100644 --- a/electron/cqrs/commands/handlers/saveRoom.ts +++ b/electron/cqrs/commands/handlers/saveRoom.ts @@ -1,7 +1,8 @@ import { DataSource } from 'typeorm'; -import { RoomEntity } from '../../../entities'; +import { RoomEntity, RoomOwnerEntity } from '../../../entities'; import { replaceRoomRelations } from '../../relations'; import { SaveRoomCommand } from '../../types'; +import { getCurrentUserScope } from '../../current-user-scope'; function extractSlowModeInterval(room: SaveRoomCommand['payload']['room']): number { if (typeof room.slowModeInterval === 'number' && Number.isFinite(room.slowModeInterval)) { @@ -21,6 +22,7 @@ export async function handleSaveRoom(command: SaveRoomCommand, dataSource: DataS const { room } = command.payload; await dataSource.transaction(async (manager) => { + const currentUserId = await getCurrentUserScope(manager); const repo = manager.getRepository(RoomEntity); const entity = repo.create({ id: room.id, @@ -43,6 +45,15 @@ export async function handleSaveRoom(command: SaveRoomCommand, dataSource: DataS }); await repo.save(entity); + + if (currentUserId) { + await manager.getRepository(RoomOwnerEntity).save({ + roomId: room.id, + userId: currentUserId, + savedAt: Date.now() + }); + } + await replaceRoomRelations(manager, room.id, { channels: room.channels ?? [], members: room.members ?? [], diff --git a/electron/cqrs/commands/handlers/updateMessage.ts b/electron/cqrs/commands/handlers/updateMessage.ts index 035ab39..e53fd20 100644 --- a/electron/cqrs/commands/handlers/updateMessage.ts +++ b/electron/cqrs/commands/handlers/updateMessage.ts @@ -2,13 +2,20 @@ import { DataSource } from 'typeorm'; import { MessageEntity } from '../../../entities'; import { replaceMessageReactions } from '../../relations'; import { UpdateMessageCommand } from '../../types'; +import { getCurrentUserScope } from '../../current-user-scope'; export async function handleUpdateMessage(command: UpdateMessageCommand, dataSource: DataSource): Promise { const { messageId, updates } = command.payload; await dataSource.transaction(async (manager) => { + const currentUserId = await getCurrentUserScope(manager); + + if (!currentUserId) { + return; + } + const repo = manager.getRepository(MessageEntity); - const existing = await repo.findOne({ where: { id: messageId } }); + const existing = await repo.findOne({ where: { id: messageId, ownerUserId: currentUserId } }); if (!existing) return; diff --git a/electron/cqrs/commands/handlers/updateRoom.ts b/electron/cqrs/commands/handlers/updateRoom.ts index d402fac..be41dc9 100644 --- a/electron/cqrs/commands/handlers/updateRoom.ts +++ b/electron/cqrs/commands/handlers/updateRoom.ts @@ -7,6 +7,7 @@ import { boolToInt, TransformMap } from './utils/applyUpdates'; +import { getCurrentUserScope, userOwnsRoom } from '../../current-user-scope'; const ROOM_TRANSFORMS: TransformMap = { hasPassword: boolToInt, @@ -32,6 +33,12 @@ export async function handleUpdateRoom(command: UpdateRoomCommand, dataSource: D const { roomId, updates } = command.payload; await dataSource.transaction(async (manager) => { + const currentUserId = await getCurrentUserScope(manager); + + if (!await userOwnsRoom(manager, roomId, currentUserId)) { + return; + } + const repo = manager.getRepository(RoomEntity); const existing = await repo.findOne({ where: { id: roomId } }); diff --git a/electron/cqrs/commands/index.ts b/electron/cqrs/commands/index.ts index 9034095..6e78dcf 100644 --- a/electron/cqrs/commands/index.ts +++ b/electron/cqrs/commands/index.ts @@ -18,7 +18,10 @@ import { SaveBanCommand, RemoveBanCommand, SaveAttachmentCommand, - DeleteAttachmentsForMessageCommand + DeleteAttachmentsForMessageCommand, + SavePluginDataCommand, + DeletePluginDataCommand, + SaveMetaCommand } from '../types'; import { handleSaveMessage } from './handlers/saveMessage'; import { handleDeleteMessage } from './handlers/deleteMessage'; @@ -36,6 +39,9 @@ import { handleSaveBan } from './handlers/saveBan'; import { handleRemoveBan } from './handlers/removeBan'; import { handleSaveAttachment } from './handlers/saveAttachment'; import { handleDeleteAttachmentsForMessage } from './handlers/deleteAttachmentsForMessage'; +import { handleSavePluginData } from './handlers/savePluginData'; +import { handleDeletePluginData } from './handlers/deletePluginData'; +import { handleSaveMeta } from './handlers/saveMeta'; import { handleClearAllData } from './handlers/clearAllData'; export const buildCommandHandlers = (dataSource: DataSource): Record Promise> => ({ @@ -55,5 +61,8 @@ export const buildCommandHandlers = (dataSource: DataSource): Record handleRemoveBan(cmd as RemoveBanCommand, dataSource), [CommandType.SaveAttachment]: (cmd) => handleSaveAttachment(cmd as SaveAttachmentCommand, dataSource), [CommandType.DeleteAttachmentsForMessage]: (cmd) => handleDeleteAttachmentsForMessage(cmd as DeleteAttachmentsForMessageCommand, dataSource), + [CommandType.SavePluginData]: (cmd) => handleSavePluginData(cmd as SavePluginDataCommand, dataSource), + [CommandType.DeletePluginData]: (cmd) => handleDeletePluginData(cmd as DeletePluginDataCommand, dataSource), + [CommandType.SaveMeta]: (cmd) => handleSaveMeta(cmd as SaveMetaCommand, dataSource), [CommandType.ClearAllData]: () => handleClearAllData(dataSource) }); diff --git a/electron/cqrs/current-user-scope.ts b/electron/cqrs/current-user-scope.ts new file mode 100644 index 0000000..77b6795 --- /dev/null +++ b/electron/cqrs/current-user-scope.ts @@ -0,0 +1,24 @@ +import { DataSource, EntityManager } from 'typeorm'; +import { MetaEntity, RoomOwnerEntity } from '../entities'; + +export async function getCurrentUserScope(dataSourceOrManager: DataSource | EntityManager): Promise { + const repo = dataSourceOrManager.getRepository(MetaEntity); + const meta = await repo.findOne({ where: { key: 'currentUserId' } }); + + return meta?.value?.trim() || null; +} + +export async function userOwnsRoom( + dataSourceOrManager: DataSource | EntityManager, + roomId: string, + userId: string | null +): Promise { + if (!userId) { + return false; + } + + const repo = dataSourceOrManager.getRepository(RoomOwnerEntity); + const owner = await repo.findOne({ where: { roomId, userId } }); + + return !!owner; +} diff --git a/electron/cqrs/queries/handlers/getAllRooms.ts b/electron/cqrs/queries/handlers/getAllRooms.ts index 19ccc1f..6a4bdf4 100644 --- a/electron/cqrs/queries/handlers/getAllRooms.ts +++ b/electron/cqrs/queries/handlers/getAllRooms.ts @@ -1,11 +1,28 @@ import { DataSource } from 'typeorm'; -import { RoomEntity } from '../../../entities'; +import { RoomEntity, RoomOwnerEntity } from '../../../entities'; import { rowToRoom } from '../../mappers'; import { loadRoomRelationsMap } from '../../relations'; +import { getCurrentUserScope } from '../../current-user-scope'; export async function handleGetAllRooms(dataSource: DataSource) { + const currentUserId = await getCurrentUserScope(dataSource); + + if (!currentUserId) { + return []; + } + const repo = dataSource.getRepository(RoomEntity); - const rows = await repo.find(); + const ownershipRows = await dataSource.getRepository(RoomOwnerEntity).find({ where: { userId: currentUserId } }); + const roomIds = ownershipRows.map((owner) => owner.roomId); + + if (roomIds.length === 0) { + return []; + } + + const rows = await repo + .createQueryBuilder('room') + .where('room.id IN (:...roomIds)', { roomIds }) + .getMany(); const relationsByRoomId = await loadRoomRelationsMap(dataSource, rows.map((row) => row.id)); return rows.map((row) => rowToRoom(row, relationsByRoomId.get(row.id))); diff --git a/electron/cqrs/queries/handlers/getCurrentUserId.ts b/electron/cqrs/queries/handlers/getCurrentUserId.ts index b9cac48..f70ef49 100644 --- a/electron/cqrs/queries/handlers/getCurrentUserId.ts +++ b/electron/cqrs/queries/handlers/getCurrentUserId.ts @@ -6,4 +6,4 @@ export async function handleGetCurrentUserId(dataSource: DataSource): Promise { + const meta = await dataSource.getRepository(MetaEntity).findOne({ + where: { key: query.payload.key } + }); + + return meta?.value ?? null; +} diff --git a/electron/cqrs/queries/handlers/getPluginData.ts b/electron/cqrs/queries/handlers/getPluginData.ts new file mode 100644 index 0000000..d7bad32 --- /dev/null +++ b/electron/cqrs/queries/handlers/getPluginData.ts @@ -0,0 +1,33 @@ +import { DataSource } from 'typeorm'; +import { getCurrentUserScope } from '../../current-user-scope'; +import { PluginDataEntity } from '../../../entities'; +import { GetPluginDataQuery } from '../../types'; + +export async function handleGetPluginData(query: GetPluginDataQuery, dataSource: DataSource): Promise { + const { payload } = query; + const ownerUserId = await getCurrentUserScope(dataSource); + + if (!ownerUserId) { + return null; + } + + const record = await dataSource.getRepository(PluginDataEntity).findOne({ + where: { + key: payload.key, + ownerUserId, + pluginId: payload.pluginId, + scope: payload.scope, + serverId: payload.serverId ?? '' + } + }); + + if (!record) { + return null; + } + + try { + return JSON.parse(record.valueJson) as unknown; + } catch { + return null; + } +} diff --git a/electron/cqrs/queries/handlers/getRoom.ts b/electron/cqrs/queries/handlers/getRoom.ts index 8de0b54..446f19d 100644 --- a/electron/cqrs/queries/handlers/getRoom.ts +++ b/electron/cqrs/queries/handlers/getRoom.ts @@ -3,8 +3,15 @@ import { RoomEntity } from '../../../entities'; import { GetRoomQuery } from '../../types'; import { rowToRoom } from '../../mappers'; import { loadRoomRelationsMap } from '../../relations'; +import { getCurrentUserScope, userOwnsRoom } from '../../current-user-scope'; export async function handleGetRoom(query: GetRoomQuery, dataSource: DataSource) { + const currentUserId = await getCurrentUserScope(dataSource); + + if (!await userOwnsRoom(dataSource, query.payload.roomId, currentUserId)) { + return null; + } + const repo = dataSource.getRepository(RoomEntity); const row = await repo.findOne({ where: { id: query.payload.roomId } }); diff --git a/electron/cqrs/queries/index.ts b/electron/cqrs/queries/index.ts index 85bf63b..ef90d28 100644 --- a/electron/cqrs/queries/index.ts +++ b/electron/cqrs/queries/index.ts @@ -8,11 +8,12 @@ import { GetMessageByIdQuery, GetReactionsForMessageQuery, GetUserQuery, - GetCurrentUserIdQuery, GetRoomQuery, GetBansForRoomQuery, IsUserBannedQuery, - GetAttachmentsForMessageQuery + GetAttachmentsForMessageQuery, + GetPluginDataQuery, + GetMetaQuery } from '../types'; import { handleGetMessages } from './handlers/getMessages'; import { handleGetMessagesSince } from './handlers/getMessagesSince'; @@ -28,6 +29,8 @@ import { handleGetBansForRoom } from './handlers/getBansForRoom'; import { handleIsUserBanned } from './handlers/isUserBanned'; import { handleGetAttachmentsForMessage } from './handlers/getAttachmentsForMessage'; import { handleGetAllAttachments } from './handlers/getAllAttachments'; +import { handleGetPluginData } from './handlers/getPluginData'; +import { handleGetMeta } from './handlers/getMeta'; export const buildQueryHandlers = (dataSource: DataSource): Record Promise> => ({ [QueryType.GetMessages]: (query) => handleGetMessages(query as GetMessagesQuery, dataSource), @@ -43,5 +46,7 @@ export const buildQueryHandlers = (dataSource: DataSource): Record handleGetBansForRoom(query as GetBansForRoomQuery, dataSource), [QueryType.IsUserBanned]: (query) => handleIsUserBanned(query as IsUserBannedQuery, dataSource), [QueryType.GetAttachmentsForMessage]: (query) => handleGetAttachmentsForMessage(query as GetAttachmentsForMessageQuery, dataSource), - [QueryType.GetAllAttachments]: () => handleGetAllAttachments(dataSource) + [QueryType.GetAllAttachments]: () => handleGetAllAttachments(dataSource), + [QueryType.GetPluginData]: (query) => handleGetPluginData(query as GetPluginDataQuery, dataSource), + [QueryType.GetMeta]: (query) => handleGetMeta(query as GetMetaQuery, dataSource) }); diff --git a/electron/cqrs/types.ts b/electron/cqrs/types.ts index 5c30f1e..3f39bdb 100644 --- a/electron/cqrs/types.ts +++ b/electron/cqrs/types.ts @@ -15,6 +15,9 @@ export const CommandType = { RemoveBan: 'remove-ban', SaveAttachment: 'save-attachment', DeleteAttachmentsForMessage: 'delete-attachments-for-message', + SavePluginData: 'save-plugin-data', + DeletePluginData: 'delete-plugin-data', + SaveMeta: 'save-meta', ClearAllData: 'clear-all-data' } as const; @@ -34,7 +37,9 @@ export const QueryType = { GetBansForRoom: 'get-bans-for-room', IsUserBanned: 'is-user-banned', GetAttachmentsForMessage: 'get-attachments-for-message', - GetAllAttachments: 'get-all-attachments' + GetAllAttachments: 'get-all-attachments', + GetPluginData: 'get-plugin-data', + GetMeta: 'get-meta' } as const; export type QueryTypeKey = typeof QueryType[keyof typeof QueryType]; @@ -172,6 +177,16 @@ export interface AttachmentPayload { savedPath?: string; } +export type PluginDataScopePayload = 'local' | 'server'; + +export interface PluginDataPayload { + key: string; + pluginId: string; + scope: PluginDataScopePayload; + serverId?: string; + value: unknown; +} + export interface SaveMessageCommand { type: typeof CommandType.SaveMessage; payload: { message: MessagePayload } } export interface DeleteMessageCommand { type: typeof CommandType.DeleteMessage; payload: { messageId: string } } export interface UpdateMessageCommand { type: typeof CommandType.UpdateMessage; payload: { messageId: string; updates: Partial } } @@ -188,6 +203,9 @@ export interface SaveBanCommand { type: typeof CommandType.SaveBan; payload: { b export interface RemoveBanCommand { type: typeof CommandType.RemoveBan; payload: { oderId: string } } export interface SaveAttachmentCommand { type: typeof CommandType.SaveAttachment; payload: { attachment: AttachmentPayload } } export interface DeleteAttachmentsForMessageCommand { type: typeof CommandType.DeleteAttachmentsForMessage; payload: { messageId: string } } +export interface SavePluginDataCommand { type: typeof CommandType.SavePluginData; payload: PluginDataPayload } +export interface DeletePluginDataCommand { type: typeof CommandType.DeletePluginData; payload: Omit } +export interface SaveMetaCommand { type: typeof CommandType.SaveMeta; payload: { key: string; value: string | null } } export interface ClearAllDataCommand { type: typeof CommandType.ClearAllData; payload: Record } export type Command = @@ -207,6 +225,9 @@ export type Command = | RemoveBanCommand | SaveAttachmentCommand | DeleteAttachmentsForMessageCommand + | SavePluginDataCommand + | DeletePluginDataCommand + | SaveMetaCommand | ClearAllDataCommand; export interface GetMessagesQuery { type: typeof QueryType.GetMessages; payload: { roomId: string; limit?: number; offset?: number } } @@ -223,6 +244,8 @@ export interface GetBansForRoomQuery { type: typeof QueryType.GetBansForRoom; pa export interface IsUserBannedQuery { type: typeof QueryType.IsUserBanned; payload: { userId: string; roomId: string } } export interface GetAttachmentsForMessageQuery { type: typeof QueryType.GetAttachmentsForMessage; payload: { messageId: string } } export interface GetAllAttachmentsQuery { type: typeof QueryType.GetAllAttachments; payload: Record } +export interface GetPluginDataQuery { type: typeof QueryType.GetPluginData; payload: Omit } +export interface GetMetaQuery { type: typeof QueryType.GetMeta; payload: { key: string } } export type Query = | GetMessagesQuery @@ -238,4 +261,6 @@ export type Query = | GetBansForRoomQuery | IsUserBannedQuery | GetAttachmentsForMessageQuery - | GetAllAttachmentsQuery; + | GetAllAttachmentsQuery + | GetPluginDataQuery + | GetMetaQuery; diff --git a/electron/data-archive.ts b/electron/data-archive.ts index d6406dc..4fa4384 100644 --- a/electron/data-archive.ts +++ b/electron/data-archive.ts @@ -22,12 +22,12 @@ const ZIP_UTF8_FLAG = 0x0800; const ZIP_STORE_METHOD = 0; const ZIP_VERSION = 20; const MAX_UINT32 = 0xffffffff; - const crcTable = buildCrcTable(); export function createZipArchive(entries: ZipArchiveEntry[]): Buffer { const localParts: Buffer[] = []; const centralEntries: CentralDirectoryEntry[] = []; + let offset = 0; for (const entry of entries) { @@ -93,7 +93,6 @@ export function createZipArchive(entries: ZipArchiveEntry[]): Buffer { return Buffer.concat([header, entry.name]); }); - const centralDirectorySize = offset - centralDirectoryOffset; if (centralEntries.length > 0xffff || centralDirectoryOffset > MAX_UINT32 || centralDirectorySize > MAX_UINT32) { @@ -111,7 +110,11 @@ export function createZipArchive(entries: ZipArchiveEntry[]): Buffer { end.writeUInt32LE(centralDirectoryOffset, 16); end.writeUInt16LE(0, 20); - return Buffer.concat([...localParts, ...centralParts, end]); + return Buffer.concat([ + ...localParts, + ...centralParts, + end + ]); } export function readZipArchive(data: Buffer): ZipArchiveEntry[] { @@ -124,6 +127,7 @@ export function readZipArchive(data: Buffer): ZipArchiveEntry[] { const entryCount = data.readUInt16LE(endOffset + 10); const centralDirectoryOffset = data.readUInt32LE(endOffset + 16); const entries: ZipArchiveEntry[] = []; + let offset = centralDirectoryOffset; for (let index = 0; index < entryCount; index += 1) { diff --git a/electron/data-management.ts b/electron/data-management.ts index ee0b65a..363c934 100644 --- a/electron/data-management.ts +++ b/electron/data-management.ts @@ -43,12 +43,11 @@ export async function openCurrentDataFolder(): Promise { export async function exportUserData(): Promise { const dataPath = app.getPath('userData'); - const defaultFileName = `metoyou-data-${new Date().toISOString().slice(0, 10)}.dat`; + const defaultFileName = `metoyou-data-${new Date().toISOString() + .slice(0, 10)}.dat`; const { canceled, filePath } = await dialog.showSaveDialog({ defaultPath: path.join(app.getPath('documents'), defaultFileName), - filters: [ - { extensions: ['dat'], name: 'MetoYou data archive' } - ], + filters: [{ extensions: ['dat'], name: 'MetoYou data archive' }], title: 'Export MetoYou data' }); @@ -88,9 +87,7 @@ export async function exportUserData(): Promise { export async function importUserData(): Promise { const { canceled, filePaths } = await dialog.showOpenDialog({ - filters: [ - { extensions: ['dat', 'zip'], name: 'MetoYou data archive' } - ], + filters: [{ extensions: ['dat', 'zip'], name: 'MetoYou data archive' }], properties: ['openFile'], title: 'Import MetoYou data' }); @@ -184,7 +181,8 @@ async function collectDataFiles(directoryPath: string): Promise { async function moveCurrentDataAside(): Promise { const dataPath = app.getPath('userData'); const backupRoot = path.join(dataPath, BACKUP_DIRECTORY_NAME); - const backupPath = path.join(backupRoot, `before-import-${new Date().toISOString().replace(/[:.]/g, '-')}`); + const backupPath = path.join(backupRoot, `before-import-${new Date().toISOString() + .replace(/[:.]/g, '-')}`); const entries = await fsp.readdir(dataPath, { withFileTypes: true }).catch(() => []); await fsp.mkdir(backupPath, { recursive: true }); @@ -204,6 +202,7 @@ async function moveCurrentDataAside(): Promise { await copyPath(sourcePath, targetPath); await fsp.rm(sourcePath, { force: true, recursive: true }); }); + movedAny = true; } diff --git a/electron/data-source.ts b/electron/data-source.ts index 137b07b..5361216 100644 --- a/electron/data-source.ts +++ b/electron/data-source.ts @@ -25,7 +25,8 @@ import { ReactionEntity, BanEntity, AttachmentEntity, - MetaEntity + MetaEntity, + PluginDataEntity } from './entities'; const projectRootDatabaseFilePath = path.join(__dirname, '..', '..', settings.databaseName); @@ -51,7 +52,8 @@ export const AppDataSource = new DataSource({ ReactionEntity, BanEntity, AttachmentEntity, - MetaEntity + MetaEntity, + PluginDataEntity ], migrations: [path.join(__dirname, 'migrations', '*.{ts,js}')], synchronize: false, diff --git a/electron/db/database.ts b/electron/db/database.ts index 93d545b..89e2b47 100644 --- a/electron/db/database.ts +++ b/electron/db/database.ts @@ -8,6 +8,7 @@ import { MessageEntity, UserEntity, RoomEntity, + RoomOwnerEntity, RoomChannelEntity, RoomMemberEntity, RoomRoleEntity, @@ -16,7 +17,8 @@ import { ReactionEntity, BanEntity, AttachmentEntity, - MetaEntity + MetaEntity, + PluginDataEntity } from '../entities'; import { settings } from '../settings'; @@ -26,8 +28,18 @@ let dbBackupPath = ''; // SQLite files start with this 16-byte header string. const SQLITE_MAGIC = 'SQLite format 3\0'; -const SAVE_RETRY_DELAYS_MS = [25, 75, 150, 300, 600]; -const RETRYABLE_SAVE_ERROR_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']); +const SAVE_RETRY_DELAYS_MS = [ + 25, + 75, + 150, + 300, + 600 +]; +const RETRYABLE_SAVE_ERROR_CODES = new Set([ + 'EPERM', + 'EACCES', + 'EBUSY' +]); let saveQueue: Promise = Promise.resolve(); @@ -163,6 +175,7 @@ export async function initializeDatabase(): Promise { MessageEntity, UserEntity, RoomEntity, + RoomOwnerEntity, RoomChannelEntity, RoomMemberEntity, RoomRoleEntity, @@ -171,7 +184,8 @@ export async function initializeDatabase(): Promise { ReactionEntity, BanEntity, AttachmentEntity, - MetaEntity + MetaEntity, + PluginDataEntity ], migrations: [path.join(__dirname, '..', 'migrations', '*.js'), path.join(__dirname, '..', 'migrations', '*.ts')], synchronize: false, diff --git a/electron/desktop-settings.ts b/electron/desktop-settings.ts index 041248b..e7ff626 100644 --- a/electron/desktop-settings.ts +++ b/electron/desktop-settings.ts @@ -4,11 +4,21 @@ import * as path from 'path'; export type AutoUpdateMode = 'auto' | 'off' | 'version'; +export interface LocalApiSettings { + enabled: boolean; + port: number; + exposeOnLan: boolean; + scalarEnabled: boolean; + docusaurusEnabled: boolean; + allowedSignalingServers: string[]; +} + export interface DesktopSettings { autoUpdateMode: AutoUpdateMode; autoStart: boolean; closeToTray: boolean; hardwareAcceleration: boolean; + localApi: LocalApiSettings; manifestUrls: string[]; preferredVersion: string | null; vaapiVideoEncode: boolean; @@ -19,11 +29,20 @@ export interface DesktopSettingsSnapshot extends DesktopSettings { restartRequired: boolean; } +const DEFAULT_LOCAL_API_SETTINGS: LocalApiSettings = { + enabled: false, + port: 17878, + exposeOnLan: false, + scalarEnabled: false, + docusaurusEnabled: false, + allowedSignalingServers: [] +}; const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { autoUpdateMode: 'auto', autoStart: true, closeToTray: true, hardwareAcceleration: true, + localApi: { ...DEFAULT_LOCAL_API_SETTINGS }, manifestUrls: [], preferredVersion: null, vaapiVideoEncode: false @@ -61,6 +80,61 @@ function normalizeManifestUrls(value: unknown): string[] { return manifestUrls; } +function normalizePort(value: unknown, fallback: number): number { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return fallback; + } + + const port = Math.floor(value); + + if (port < 1 || port > 65535) { + return fallback; + } + + return port; +} + +function normalizeAllowedSignalingServers(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + + const urls: string[] = []; + + for (const entry of value) { + if (typeof entry !== 'string') { + continue; + } + + const trimmed = entry.trim().replace(/\/+$/u, ''); + + if (!trimmed || urls.includes(trimmed)) { + continue; + } + + if (!/^https?:\/\//iu.test(trimmed)) { + continue; + } + + urls.push(trimmed); + } + + return urls; +} + +function normalizeLocalApiSettings(value: unknown): LocalApiSettings { + const source = (value && typeof value === 'object') ? value as Partial : {}; + + return { + enabled: typeof source.enabled === 'boolean' ? source.enabled : DEFAULT_LOCAL_API_SETTINGS.enabled, + port: normalizePort(source.port, DEFAULT_LOCAL_API_SETTINGS.port), + exposeOnLan: typeof source.exposeOnLan === 'boolean' ? source.exposeOnLan : DEFAULT_LOCAL_API_SETTINGS.exposeOnLan, + scalarEnabled: typeof source.scalarEnabled === 'boolean' ? source.scalarEnabled : DEFAULT_LOCAL_API_SETTINGS.scalarEnabled, + docusaurusEnabled: typeof source.docusaurusEnabled === 'boolean' ? source.docusaurusEnabled : DEFAULT_LOCAL_API_SETTINGS.docusaurusEnabled, + allowedSignalingServers: normalizeAllowedSignalingServers(source.allowedSignalingServers) + }; +} + export function getDesktopSettingsSnapshot(): DesktopSettingsSnapshot { const storedSettings = readDesktopSettings(); const runtimeHardwareAcceleration = app.isHardwareAccelerationEnabled(); @@ -97,6 +171,7 @@ export function readDesktopSettings(): DesktopSettings { hardwareAcceleration: typeof parsed.hardwareAcceleration === 'boolean' ? parsed.hardwareAcceleration : DEFAULT_DESKTOP_SETTINGS.hardwareAcceleration, + localApi: normalizeLocalApiSettings(parsed.localApi), manifestUrls: normalizeManifestUrls(parsed.manifestUrls), preferredVersion: normalizePreferredVersion(parsed.preferredVersion) }; @@ -106,9 +181,13 @@ export function readDesktopSettings(): DesktopSettings { } export function updateDesktopSettings(patch: Partial): DesktopSettingsSnapshot { + const previousSettings = readDesktopSettings(); const mergedSettings = { - ...readDesktopSettings(), - ...patch + ...previousSettings, + ...patch, + localApi: patch.localApi + ? { ...previousSettings.localApi, ...patch.localApi } + : previousSettings.localApi }; const nextSettings: DesktopSettings = { autoUpdateMode: normalizeAutoUpdateMode(mergedSettings.autoUpdateMode), @@ -121,6 +200,7 @@ export function updateDesktopSettings(patch: Partial): DesktopS hardwareAcceleration: typeof mergedSettings.hardwareAcceleration === 'boolean' ? mergedSettings.hardwareAcceleration : DEFAULT_DESKTOP_SETTINGS.hardwareAcceleration, + localApi: normalizeLocalApiSettings(mergedSettings.localApi), manifestUrls: normalizeManifestUrls(mergedSettings.manifestUrls), preferredVersion: normalizePreferredVersion(mergedSettings.preferredVersion), vaapiVideoEncode: typeof mergedSettings.vaapiVideoEncode === 'boolean' diff --git a/electron/entities/MessageEntity.ts b/electron/entities/MessageEntity.ts index 0f6080c..26810b3 100644 --- a/electron/entities/MessageEntity.ts +++ b/electron/entities/MessageEntity.ts @@ -12,6 +12,9 @@ export class MessageEntity { @Column('text') roomId!: string; + @Column('text', { nullable: true }) + ownerUserId!: string | null; + @Column('text', { nullable: true }) channelId!: string | null; diff --git a/electron/entities/PluginDataEntity.ts b/electron/entities/PluginDataEntity.ts new file mode 100644 index 0000000..020ce96 --- /dev/null +++ b/electron/entities/PluginDataEntity.ts @@ -0,0 +1,29 @@ +import { + Column, + Entity, + PrimaryColumn +} from 'typeorm'; + +@Entity('plugin_data') +export class PluginDataEntity { + @PrimaryColumn('text') + ownerUserId!: string; + + @PrimaryColumn('text') + pluginId!: string; + + @PrimaryColumn('text') + scope!: string; + + @PrimaryColumn('text') + serverId!: string; + + @PrimaryColumn('text') + key!: string; + + @Column('text') + valueJson!: string; + + @Column('integer') + updatedAt!: number; +} diff --git a/electron/entities/RoomOwnerEntity.ts b/electron/entities/RoomOwnerEntity.ts new file mode 100644 index 0000000..f95918a --- /dev/null +++ b/electron/entities/RoomOwnerEntity.ts @@ -0,0 +1,19 @@ +import { + Column, + Entity, + Index, + PrimaryColumn +} from 'typeorm'; + +@Entity('room_owners') +export class RoomOwnerEntity { + @PrimaryColumn('text') + roomId!: string; + + @PrimaryColumn('text') + @Index() + userId!: string; + + @Column('integer') + savedAt!: number; +} diff --git a/electron/entities/index.ts b/electron/entities/index.ts index cf55a34..3cea0a7 100644 --- a/electron/entities/index.ts +++ b/electron/entities/index.ts @@ -1,6 +1,7 @@ export { MessageEntity } from './MessageEntity'; export { UserEntity } from './UserEntity'; export { RoomEntity } from './RoomEntity'; +export { RoomOwnerEntity } from './RoomOwnerEntity'; export { RoomChannelEntity } from './RoomChannelEntity'; export { RoomMemberEntity } from './RoomMemberEntity'; export { RoomRoleEntity } from './RoomRoleEntity'; @@ -10,3 +11,4 @@ export { ReactionEntity } from './ReactionEntity'; export { BanEntity } from './BanEntity'; export { AttachmentEntity } from './AttachmentEntity'; export { MetaEntity } from './MetaEntity'; +export { PluginDataEntity } from './PluginDataEntity'; diff --git a/electron/ipc/system.ts b/electron/ipc/system.ts index 4bad4f5..8fb8d92 100644 --- a/electron/ipc/system.ts +++ b/electron/ipc/system.ts @@ -18,6 +18,7 @@ import { updateDesktopSettings, type DesktopSettings } from '../desktop-settings'; +import { applyLocalApiSettings, getLocalApiSnapshot } from '../api'; import { activateLinuxScreenShareAudioRouting, deactivateLinuxScreenShareAudioRouting, @@ -49,6 +50,7 @@ import { readSavedTheme, writeSavedTheme } from '../theme-library'; +import { getLocalPluginsPath, listLocalPluginManifests } from '../plugin-library'; import { eraseUserData, exportUserData, @@ -349,6 +351,8 @@ export function setupSystemHandlers(): void { ipcMain.handle('import-user-data', async () => await importUserData()); ipcMain.handle('erase-user-data', async () => await eraseUserData()); ipcMain.handle('get-saved-themes-path', async () => await getSavedThemesPath()); + ipcMain.handle('get-local-plugins-path', async () => await getLocalPluginsPath()); + ipcMain.handle('list-local-plugin-manifests', async () => await listLocalPluginManifests()); ipcMain.handle('list-saved-themes', async () => await listSavedThemes()); ipcMain.handle('read-saved-theme', async (_event, fileName: string) => await readSavedTheme(fileName)); ipcMain.handle('write-saved-theme', async (_event, fileName: string, text: string) => { @@ -449,9 +453,57 @@ export function setupSystemHandlers(): void { await synchronizeAutoStartSetting(snapshot.autoStart); updateCloseToTraySetting(snapshot.closeToTray); await handleDesktopSettingsChanged(); + await applyLocalApiSettings(); return snapshot; }); + ipcMain.handle('get-local-api-status', () => getLocalApiSnapshot()); + + ipcMain.handle('open-local-api-docs', async () => { + const snapshot = getLocalApiSnapshot(); + + if (snapshot.status !== 'running' || !snapshot.baseUrl) { + return { opened: false, reason: 'Local API is not running' }; + } + + if (!snapshot.scalarEnabled) { + return { opened: false, reason: 'Scalar docs are disabled' }; + } + + await shell.openExternal(`${snapshot.baseUrl}/docs`); + return { opened: true }; + }); + + ipcMain.handle('open-docusaurus-docs', async () => { + let snapshot = getLocalApiSnapshot(); + + if (snapshot.status !== 'running' || !snapshot.baseUrl || !snapshot.docusaurusEnabled) { + const currentSettings = getDesktopSettingsSnapshot(); + + updateDesktopSettings({ + localApi: { + ...currentSettings.localApi, + enabled: true, + docusaurusEnabled: true + } + }); + + await applyLocalApiSettings(); + snapshot = getLocalApiSnapshot(); + } + + if (snapshot.status !== 'running' || !snapshot.baseUrl) { + return { opened: false, reason: snapshot.error ?? 'Local documentation server is not running' }; + } + + if (!snapshot.docusaurusEnabled) { + return { opened: false, reason: 'Docusaurus docs are disabled' }; + } + + await shell.openExternal(`${snapshot.baseUrl}/docusaurus/`); + return { opened: true }; + }); + ipcMain.handle('relaunch-app', () => { app.relaunch(); app.exit(0); diff --git a/electron/migrations/1000000000008-AddPluginData.ts b/electron/migrations/1000000000008-AddPluginData.ts new file mode 100644 index 0000000..fbcbe6a --- /dev/null +++ b/electron/migrations/1000000000008-AddPluginData.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPluginData1000000000008 implements MigrationInterface { + name = 'AddPluginData1000000000008'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "plugin_data" ( + "pluginId" TEXT NOT NULL, + "scope" TEXT NOT NULL, + "serverId" TEXT NOT NULL DEFAULT '', + "key" TEXT NOT NULL, + "valueJson" TEXT NOT NULL, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("pluginId", "scope", "serverId", "key") + ) + `); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_plugin_data_plugin_scope" ON "plugin_data" ("pluginId", "scope")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS "idx_plugin_data_plugin_scope"`); + await queryRunner.query(`DROP TABLE IF EXISTS "plugin_data"`); + } +} diff --git a/electron/migrations/1000000000009-UserScopedRoomsAndMessages.ts b/electron/migrations/1000000000009-UserScopedRoomsAndMessages.ts new file mode 100644 index 0000000..4d49d0f --- /dev/null +++ b/electron/migrations/1000000000009-UserScopedRoomsAndMessages.ts @@ -0,0 +1,50 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UserScopedRoomsAndMessages1000000000009 implements MigrationInterface { + name = 'UserScopedRoomsAndMessages1000000000009'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "room_owners" ( + "roomId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "savedAt" INTEGER NOT NULL, + PRIMARY KEY ("roomId", "userId") + ) + `); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_room_owners_userId" ON "room_owners" ("userId")`); + + const columns = await queryRunner.query(`PRAGMA table_info("messages")`) as Array<{ name?: string }>; + const hasOwnerUserId = columns.some((column) => column.name === 'ownerUserId'); + + if (!hasOwnerUserId) { + await queryRunner.query(`ALTER TABLE "messages" ADD COLUMN "ownerUserId" TEXT`); + } + + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_messages_owner_room" ON "messages" ("ownerUserId", "roomId")`); + + const metaRows = await queryRunner.query(`SELECT "value" FROM "meta" WHERE "key" = 'currentUserId' LIMIT 1`) as Array<{ value?: string | null }>; + const currentUserId = metaRows[0]?.value?.trim(); + + if (!currentUserId) { + return; + } + + const now = Date.now(); + + await queryRunner.query( + `INSERT OR IGNORE INTO "room_owners" ("roomId", "userId", "savedAt") SELECT "id", ?, ? FROM "rooms"`, + [currentUserId, now] + ); + await queryRunner.query( + `UPDATE "messages" SET "ownerUserId" = ? WHERE "ownerUserId" IS NULL`, + [currentUserId] + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS "idx_messages_owner_room"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_room_owners_userId"`); + await queryRunner.query(`DROP TABLE IF EXISTS "room_owners"`); + } +} \ No newline at end of file diff --git a/electron/migrations/1000000000010-UserScopedPluginData.ts b/electron/migrations/1000000000010-UserScopedPluginData.ts new file mode 100644 index 0000000..6a9a011 --- /dev/null +++ b/electron/migrations/1000000000010-UserScopedPluginData.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UserScopedPluginData1000000000010 implements MigrationInterface { + name = 'UserScopedPluginData1000000000010'; + + public async up(queryRunner: QueryRunner): Promise { + const columns = await queryRunner.query(`PRAGMA table_info("plugin_data")`) as Array<{ name?: string }>; + const hasOwnerUserId = columns.some((column) => column.name === 'ownerUserId'); + + if (hasOwnerUserId) { + return; + } + + const metaRows = await queryRunner.query(`SELECT "value" FROM "meta" WHERE "key" = 'currentUserId' LIMIT 1`) as Array<{ value?: string | null }>; + const currentUserId = metaRows[0]?.value?.trim() ?? ''; + + await queryRunner.query(` + CREATE TABLE "temporary_plugin_data" ( + "ownerUserId" TEXT NOT NULL, + "pluginId" TEXT NOT NULL, + "scope" TEXT NOT NULL, + "serverId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "valueJson" TEXT NOT NULL, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("ownerUserId", "pluginId", "scope", "serverId", "key") + ) + `); + await queryRunner.query( + `INSERT INTO "temporary_plugin_data" ("ownerUserId", "pluginId", "scope", "serverId", "key", "valueJson", "updatedAt") + SELECT ?, "pluginId", "scope", "serverId", "key", "valueJson", "updatedAt" FROM "plugin_data"`, + [currentUserId] + ); + await queryRunner.query(`DROP TABLE "plugin_data"`); + await queryRunner.query(`ALTER TABLE "temporary_plugin_data" RENAME TO "plugin_data"`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_plugin_data_owner_plugin_scope" ON "plugin_data" ("ownerUserId", "pluginId", "scope")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_plugin_data" ( + "pluginId" TEXT NOT NULL, + "scope" TEXT NOT NULL, + "serverId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "valueJson" TEXT NOT NULL, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("pluginId", "scope", "serverId", "key") + )`); + await queryRunner.query(`INSERT OR REPLACE INTO "temporary_plugin_data" ("pluginId", "scope", "serverId", "key", "valueJson", "updatedAt") + SELECT "pluginId", "scope", "serverId", "key", "valueJson", "updatedAt" FROM "plugin_data"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_plugin_data_owner_plugin_scope"`); + await queryRunner.query(`DROP TABLE "plugin_data"`); + await queryRunner.query(`ALTER TABLE "temporary_plugin_data" RENAME TO "plugin_data"`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_plugin_data_plugin_scope" ON "plugin_data" ("pluginId", "scope")`); + } +} diff --git a/electron/plugin-library.spec.ts b/electron/plugin-library.spec.ts new file mode 100644 index 0000000..d48ffff --- /dev/null +++ b/electron/plugin-library.spec.ts @@ -0,0 +1,126 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi +} from 'vitest'; +import { + cp, + mkdtemp, + mkdir, + rm, + writeFile +} from 'fs/promises'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { TEST_PLUGIN_FIXTURE_DIR, TEST_PLUGIN_ID } from '../e2e/helpers/plugin-api-test-fixture'; + +const { mockGetPath } = vi.hoisted(() => ({ + mockGetPath: vi.fn() +})); + +vi.mock('electron', () => ({ + app: { + getPath: mockGetPath + } +})); + +import { getLocalPluginsPath, listLocalPluginManifests } from './plugin-library'; + +describe('plugin-library', () => { + let userDataPath: string; + + beforeEach(async () => { + userDataPath = await mkdtemp(join(tmpdir(), 'metoyou-plugin-library-')); + mockGetPath.mockReturnValue(userDataPath); + }); + + afterEach(async () => { + await rm(userDataPath, { recursive: true, force: true }); + mockGetPath.mockReset(); + }); + + it('creates and reports the local plugins folder', async () => { + const pluginsPath = await getLocalPluginsPath(); + const result = await listLocalPluginManifests(); + + expect(pluginsPath).toBe(join(userDataPath, 'plugins')); + expect(result).toEqual({ + errors: [], + plugins: [], + pluginsPath + }); + }); + + it('discovers immediate child plugin manifests and safe relative files', async () => { + const pluginRoot = join(userDataPath, 'plugins', 'api-test-plugin'); + + await cp(TEST_PLUGIN_FIXTURE_DIR, pluginRoot, { recursive: true }); + + const result = await listLocalPluginManifests(); + + expect(result.errors).toEqual([]); + expect(result.plugins).toHaveLength(1); + expect(result.plugins[0]).toEqual(expect.objectContaining({ + entrypointPath: join(pluginRoot, 'dist', 'main.js'), + manifestPath: join(pluginRoot, 'toju-plugin.json'), + pluginRoot, + readmePath: join(pluginRoot, 'README.md') + })); + + expect(result.plugins[0]?.manifest).toEqual(expect.objectContaining({ id: TEST_PLUGIN_ID })); + }); + + it('reports invalid JSON and keeps scanning other plugins', async () => { + const invalidRoot = join(userDataPath, 'plugins', 'invalid-plugin'); + const validRoot = join(userDataPath, 'plugins', 'valid-plugin'); + + await mkdir(invalidRoot, { recursive: true }); + await mkdir(validRoot, { recursive: true }); + await writeFile(join(invalidRoot, 'plugin.json'), '{', 'utf8'); + await writeFile(join(validRoot, 'plugin.json'), JSON.stringify({ + apiVersion: '1.0.0', + compatibility: { minimumTojuVersion: '1.0.0' }, + description: 'Valid plugin', + entrypoint: './main.js', + id: 'valid.plugin', + kind: 'client', + schemaVersion: 1, + title: 'Valid Plugin', + version: '1.0.0' + }), 'utf8'); + + const result = await listLocalPluginManifests(); + + expect(result.plugins.map((plugin) => plugin.pluginRoot)).toEqual([validRoot]); + expect(result.errors).toHaveLength(1); + expect(result.errors[0]).toEqual(expect.objectContaining({ + manifestPath: join(invalidRoot, 'plugin.json'), + pluginRoot: invalidRoot + })); + }); + + it('does not resolve entrypoints outside the plugin folder', async () => { + const pluginRoot = join(userDataPath, 'plugins', 'unsafe-plugin'); + + await mkdir(pluginRoot, { recursive: true }); + await writeFile(join(userDataPath, 'plugins', 'outside.js'), 'export default {};', 'utf8'); + await writeFile(join(pluginRoot, 'plugin.json'), JSON.stringify({ + apiVersion: '1.0.0', + compatibility: { minimumTojuVersion: '1.0.0' }, + description: 'Unsafe plugin', + entrypoint: '../outside.js', + id: 'unsafe.plugin', + kind: 'client', + schemaVersion: 1, + title: 'Unsafe Plugin', + version: '1.0.0' + }), 'utf8'); + + const result = await listLocalPluginManifests(); + + expect(result.plugins[0]?.entrypointPath).toBeUndefined(); + }); +}); diff --git a/electron/plugin-library.ts b/electron/plugin-library.ts new file mode 100644 index 0000000..ef43040 --- /dev/null +++ b/electron/plugin-library.ts @@ -0,0 +1,165 @@ +import { app } from 'electron'; +import * as fsp from 'fs/promises'; +import * as path from 'path'; +import { pathToFileURL } from 'url'; + +const PLUGINS_FOLDER_NAME = 'plugins'; +const MANIFEST_FILE_NAMES = ['toju-plugin.json', 'plugin.json'] as const; + +export interface LocalPluginManifestDescriptor { + discoveredAt: number; + entrypointPath?: string; + pluginRootUrl: string; + manifest: unknown; + manifestPath: string; + pluginRoot: string; + readmePath?: string; +} + +export interface LocalPluginDiscoveryError { + manifestPath?: string; + message: string; + pluginRoot?: string; +} + +export interface LocalPluginDiscoveryResult { + errors: LocalPluginDiscoveryError[]; + plugins: LocalPluginManifestDescriptor[]; + pluginsPath: string; +} + +function resolvePluginsPath(): string { + return path.join(app.getPath('userData'), PLUGINS_FOLDER_NAME); +} + +async function ensurePluginsPath(): Promise { + const pluginsPath = resolvePluginsPath(); + + await fsp.mkdir(pluginsPath, { recursive: true }); + + return pluginsPath; +} + +async function realpathOrSelf(filePath: string): Promise { + try { + return await fsp.realpath(filePath); + } catch { + return filePath; + } +} + +function isPathInside(parentPath: string, candidatePath: string): boolean { + const relativePath = path.relative(parentPath, candidatePath); + + return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath); +} + +function readManifestPath(manifestRecord: Record, key: string): string | undefined { + const value = manifestRecord[key]; + + return typeof value === 'string' && value.trim() + ? value.trim() + : undefined; +} + +async function resolveManifestRelativeFile(pluginRoot: string, relativeFilePath: string | undefined): Promise { + if (!relativeFilePath || path.isAbsolute(relativeFilePath)) { + return undefined; + } + + const normalizedPath = path.normalize(relativeFilePath); + + if (normalizedPath.startsWith('..')) { + return undefined; + } + + const candidatePath = path.join(pluginRoot, normalizedPath); + const [realPluginRoot, realCandidatePath] = await Promise.all([realpathOrSelf(pluginRoot), realpathOrSelf(candidatePath)]); + + if (!isPathInside(realPluginRoot, realCandidatePath)) { + return undefined; + } + + try { + const stats = await fsp.stat(realCandidatePath); + + return stats.isFile() ? realCandidatePath : undefined; + } catch { + return undefined; + } +} + +async function findManifestPath(pluginRoot: string): Promise { + for (const fileName of MANIFEST_FILE_NAMES) { + const manifestPath = path.join(pluginRoot, fileName); + + try { + const stats = await fsp.stat(manifestPath); + + if (stats.isFile()) { + return manifestPath; + } + } catch { + // Missing manifest candidates are expected while scanning folders. + } + } + + return undefined; +} + +async function readPluginManifest(pluginRoot: string, manifestPath: string): Promise { + const text = await fsp.readFile(manifestPath, 'utf8'); + const manifest = JSON.parse(text) as unknown; + const manifestRecord = manifest && typeof manifest === 'object' && !Array.isArray(manifest) + ? manifest as Record + : {}; + const entrypointPromise = resolveManifestRelativeFile(pluginRoot, readManifestPath(manifestRecord, 'entrypoint')); + const readmePromise = resolveManifestRelativeFile(pluginRoot, readManifestPath(manifestRecord, 'readme')); + const [entrypointPath, readmePath] = await Promise.all([entrypointPromise, readmePromise]); + + return { + discoveredAt: Date.now(), + entrypointPath, + pluginRootUrl: pathToFileURL(pluginRoot + path.sep).toString(), + manifest, + manifestPath, + pluginRoot, + readmePath + }; +} + +export async function getLocalPluginsPath(): Promise { + return await ensurePluginsPath(); +} + +export async function listLocalPluginManifests(): Promise { + const pluginsPath = await ensurePluginsPath(); + const entries = await fsp.readdir(pluginsPath, { withFileTypes: true }); + const plugins: LocalPluginManifestDescriptor[] = []; + const errors: LocalPluginDiscoveryError[] = []; + + for (const entry of entries.filter((candidate) => candidate.isDirectory())) { + const pluginRoot = path.join(pluginsPath, entry.name); + const manifestPath = await findManifestPath(pluginRoot); + + if (!manifestPath) { + continue; + } + + try { + plugins.push(await readPluginManifest(pluginRoot, manifestPath)); + } catch (error) { + errors.push({ + manifestPath, + message: error instanceof Error ? error.message : 'Unable to read plugin manifest', + pluginRoot + }); + } + } + + return { + errors, + plugins: plugins.sort((left, right) => left.pluginRoot.localeCompare(right.pluginRoot)), + pluginsPath + }; +} diff --git a/electron/preload.ts b/electron/preload.ts index 22a8ce0..66f3f89 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -109,6 +109,50 @@ export interface SavedThemeFileDescriptor { path: string; } +export interface LocalPluginManifestDescriptor { + discoveredAt: number; + entrypointPath?: string; + pluginRootUrl: string; + manifest: unknown; + manifestPath: string; + pluginRoot: string; + readmePath?: string; +} + +export interface LocalApiSettings { + enabled: boolean; + port: number; + exposeOnLan: boolean; + scalarEnabled: boolean; + docusaurusEnabled: boolean; + allowedSignalingServers: string[]; +} + +export type LocalApiStatus = 'stopped' | 'starting' | 'running' | 'error'; + +export interface LocalApiSnapshot { + status: LocalApiStatus; + host: string | null; + port: number | null; + baseUrl: string | null; + error: string | null; + exposeOnLan: boolean; + scalarEnabled: boolean; + docusaurusEnabled: boolean; +} + +export interface LocalPluginDiscoveryError { + manifestPath?: string; + message: string; + pluginRoot?: string; +} + +export interface LocalPluginDiscoveryResult { + errors: LocalPluginDiscoveryError[]; + plugins: LocalPluginManifestDescriptor[]; + pluginsPath: string; +} + export interface ExportUserDataResult { cancelled: boolean; exported: boolean; @@ -181,6 +225,8 @@ export interface ElectronAPI { importUserData: () => Promise; eraseUserData: () => Promise; getSavedThemesPath: () => Promise; + getLocalPluginsPath: () => Promise; + listLocalPluginManifests: () => Promise; listSavedThemes: () => Promise; readSavedTheme: (fileName: string) => Promise; writeSavedTheme: (fileName: string, text: string) => Promise; @@ -191,10 +237,12 @@ export interface ElectronAPI { autoStart: boolean; closeToTray: boolean; hardwareAcceleration: boolean; + localApi: LocalApiSettings; manifestUrls: string[]; preferredVersion: string | null; runtimeHardwareAcceleration: boolean; restartRequired: boolean; + vaapiVideoEncode: boolean; }>; showDesktopNotification: (payload: DesktopNotificationPayload) => Promise; requestWindowAttention: () => Promise; @@ -211,6 +259,7 @@ export interface ElectronAPI { autoStart?: boolean; closeToTray?: boolean; hardwareAcceleration?: boolean; + localApi?: Partial; manifestUrls?: string[]; preferredVersion?: string | null; vaapiVideoEncode?: boolean; @@ -219,11 +268,16 @@ export interface ElectronAPI { autoStart: boolean; closeToTray: boolean; hardwareAcceleration: boolean; + localApi: LocalApiSettings; manifestUrls: string[]; preferredVersion: string | null; runtimeHardwareAcceleration: boolean; restartRequired: boolean; + vaapiVideoEncode: boolean; }>; + getLocalApiStatus: () => Promise; + openLocalApiDocs: () => Promise<{ opened: boolean; reason?: string }>; + openDocusaurusDocs: () => Promise<{ opened: boolean; reason?: string }>; relaunchApp: () => Promise; onDeepLinkReceived: (listener: (url: string) => void) => () => void; readClipboardFiles: () => Promise; @@ -294,6 +348,8 @@ const electronAPI: ElectronAPI = { importUserData: () => ipcRenderer.invoke('import-user-data'), eraseUserData: () => ipcRenderer.invoke('erase-user-data'), getSavedThemesPath: () => ipcRenderer.invoke('get-saved-themes-path'), + getLocalPluginsPath: () => ipcRenderer.invoke('get-local-plugins-path'), + listLocalPluginManifests: () => ipcRenderer.invoke('list-local-plugin-manifests'), listSavedThemes: () => ipcRenderer.invoke('list-saved-themes'), readSavedTheme: (fileName) => ipcRenderer.invoke('read-saved-theme', fileName), writeSavedTheme: (fileName, text) => ipcRenderer.invoke('write-saved-theme', fileName, text), @@ -331,6 +387,9 @@ const electronAPI: ElectronAPI = { }; }, setDesktopSettings: (patch) => ipcRenderer.invoke('set-desktop-settings', patch), + getLocalApiStatus: () => ipcRenderer.invoke('get-local-api-status'), + openLocalApiDocs: () => ipcRenderer.invoke('open-local-api-docs'), + openDocusaurusDocs: () => ipcRenderer.invoke('open-docusaurus-docs'), relaunchApp: () => ipcRenderer.invoke('relaunch-app'), onDeepLinkReceived: (listener) => { const wrappedListener = (_event: Electron.IpcRendererEvent, url: string) => { diff --git a/package-lock.json b/package-lock.json index 6047d28..5c77629 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@ngrx/entity": "^21.0.1", "@ngrx/store": "^21.0.1", "@ngrx/store-devtools": "^21.0.1", + "@scalar/api-reference": "^1.53.1", "@spartan-ng/brain": "^0.0.1-alpha.589", "@spartan-ng/cli": "^0.0.1-alpha.589", "@spartan-ng/ui-core": "^0.0.1-alpha.380", @@ -86,6 +87,69 @@ "wait-on": "^7.2.0" } }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.13.tgz", + "integrity": "sha512-g7nE4PFtngOZNZSy1lOPpkC+FAiHxqBJXqyRMEG7NUrEVZlz5goBdtHg1YgWRJIX776JTXAmbOI5JreAKVAsVA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.2", + "@ai-sdk/provider-utils": "4.0.5", + "@vercel/oidc": "3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.2.tgz", + "integrity": "sha512-HrEmNt/BH/hkQ7zpi2o6N3k1ZR1QTb7z85WYhYygiTxOQuaml4CMtHCWRbric5WPU+RNsYI7r1EpyVQMKO1pYw==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.5.tgz", + "integrity": "sha512-Ow/X/SEkeExTTc1x+nYLB9ZHK2WUId8+9TlkamAx7Tl9vxU+cKzWx2dwjgMHeCN6twrgwkLrrtqckQeO4mxgVA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.2", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/vue": { + "version": "3.0.33", + "resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-3.0.33.tgz", + "integrity": "sha512-czM9Js3a7f+Eo35gjEYEeJYUoPvMg5Dfi4bOLyDBghLqn0gaVg8yTmTaSuHCg+3K/+1xPjyXd4+2XcQIohWWiQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "4.0.5", + "ai": "6.0.33", + "swrv": "^1.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "vue": "^3.3.4" + } + }, "node_modules/@algolia/abtesting": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", @@ -1100,13 +1164,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -1412,12 +1476,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -2646,9 +2710,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2746,6 +2810,38 @@ "@lezer/css": "^1.1.7" } }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, "node_modules/@codemirror/lang-json": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", @@ -2756,6 +2852,35 @@ "@lezer/json": "^1.0.0" } }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", + "integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.12.3", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", @@ -4952,6 +5077,80 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/core/node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom/node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@floating-ui/vue": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.9.tgz", + "integrity": "sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4", + "@floating-ui/utils": "^0.2.10", + "vue-demi": ">=0.13.0" + } + }, + "node_modules/@floating-ui/vue/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@gar/promise-retry": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", @@ -4999,6 +5198,33 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@headlessui/tailwindcss": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz", + "integrity": "sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "tailwindcss": "^3.0 || ^4.0" + } + }, + "node_modules/@headlessui/vue": { + "version": "1.7.23", + "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", + "integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==", + "license": "MIT", + "dependencies": { + "@tanstack/vue-virtual": "^3.0.0-beta.60" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -5449,6 +5675,24 @@ } } }, + "node_modules/@internationalized/date": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz", + "integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.6.tgz", + "integrity": "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5826,6 +6070,28 @@ "@lezer/common": "^1.3.0" } }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, "node_modules/@lezer/json": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", @@ -5846,6 +6112,28 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", @@ -9004,6 +9292,15 @@ "node": ">=12" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@oxc-project/types": { "version": "0.96.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.96.0.tgz", @@ -9343,6 +9640,12 @@ "typescript": "^3 || ^4 || ^5" } }, + "node_modules/@phosphor-icons/core": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@phosphor-icons/core/-/core-2.1.1.tgz", + "integrity": "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -9382,6 +9685,17 @@ "node": ">=18" } }, + "node_modules/@replit/codemirror-css-color-picker": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-css-color-picker/-/codemirror-css-color-picker-6.3.0.tgz", + "integrity": "sha512-19biDANghUm7Fz7L1SNMIhK48tagaWuCOHj4oPPxc7hxPGkTVY2lU/jVZ8tsbTKQPVG7BO2CBDzs7CBwb20t4A==", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz", @@ -10233,6 +10547,565 @@ } } }, + "node_modules/@scalar/agent-chat": { + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/@scalar/agent-chat/-/agent-chat-0.10.10.tgz", + "integrity": "sha512-UYyjq6VfWzTPm1hXykyvI/F3oLjrmy2cWG9fdAc5d8XDRMeHo87s62wC7sCsupKqDfyNYxbeaUT0RLx2iXPntQ==", + "license": "MIT", + "dependencies": { + "@ai-sdk/vue": "3.0.33", + "@scalar/api-client": "3.3.1", + "@scalar/components": "0.22.5", + "@scalar/helpers": "0.5.3", + "@scalar/icons": "0.7.2", + "@scalar/json-magic": "0.12.9", + "@scalar/openapi-types": "0.8.0", + "@scalar/themes": "0.15.3", + "@scalar/types": "0.9.3", + "@scalar/use-toasts": "0.10.2", + "@scalar/workspace-store": "0.47.1", + "@vueuse/core": "13.9.0", + "ai": "6.0.33", + "js-base64": "^3.7.8", + "neverpanic": "0.0.7", + "truncate-json": "3.0.1", + "vue": "^3.5.30", + "zod": "^4.3.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/api-client": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@scalar/api-client/-/api-client-3.3.1.tgz", + "integrity": "sha512-baqrMM9T7uToNILwnqVolutDFN1YKwnErVj9+TriEwNJ3X1McVxh1qd8zCFs30/dB30hxgQWYFixdm0Zlf+NZw==", + "license": "MIT", + "dependencies": { + "@headlessui/tailwindcss": "^0.2.2", + "@headlessui/vue": "1.7.23", + "@scalar/components": "0.22.5", + "@scalar/helpers": "0.5.3", + "@scalar/icons": "0.7.2", + "@scalar/json-magic": "0.12.9", + "@scalar/oas-utils": "0.13.4", + "@scalar/openapi-types": "0.8.0", + "@scalar/postman-to-openapi": "0.7.2", + "@scalar/sidebar": "0.9.6", + "@scalar/snippetz": "0.9.3", + "@scalar/themes": "0.15.3", + "@scalar/typebox": "^0.1.3", + "@scalar/types": "0.9.3", + "@scalar/use-codemirror": "0.14.11", + "@scalar/use-hooks": "0.4.3", + "@scalar/use-toasts": "0.10.2", + "@scalar/validation": "0.3.0", + "@scalar/workspace-store": "0.47.1", + "@types/har-format": "^1.2.16", + "@vueuse/core": "13.9.0", + "@vueuse/integrations": "13.9.0", + "cookie": "1.1.1", + "focus-trap": "^7.8.0", + "fuse.js": "^7.1.0", + "js-base64": "^3.7.8", + "monaco-editor": "0.55.1", + "monaco-yaml": "5.4.1", + "nanoid": "^5.1.6", + "pretty-ms": "^9.3.0", + "radix-vue": "^1.9.17", + "set-cookie-parser": "3.1.0", + "shell-quote": "^1.8.3", + "vite-plugin-monaco-editor": "^1.1.0", + "vue": "^3.5.30", + "vue-router": "5.0.4", + "yaml": "^2.8.0", + "zod": "^4.3.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/api-client/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@scalar/api-client/node_modules/nanoid": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/api-reference": { + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/@scalar/api-reference/-/api-reference-1.53.1.tgz", + "integrity": "sha512-4z2UMrVx3EWeSjOzRA9tNonRL1NpJsd9w798HwaAxP+1rfpaBbjUjcqu0fP0eE2OfYCF4S1wS0libRAQdbhrHg==", + "license": "MIT", + "dependencies": { + "@headlessui/vue": "1.7.23", + "@scalar/agent-chat": "0.10.10", + "@scalar/api-client": "3.3.1", + "@scalar/code-highlight": "0.3.4", + "@scalar/components": "0.22.5", + "@scalar/helpers": "0.5.3", + "@scalar/icons": "0.7.2", + "@scalar/oas-utils": "0.13.4", + "@scalar/sidebar": "0.9.6", + "@scalar/snippetz": "0.9.3", + "@scalar/themes": "0.15.3", + "@scalar/types": "0.9.3", + "@scalar/use-hooks": "0.4.3", + "@scalar/use-toasts": "0.10.2", + "@scalar/workspace-store": "0.47.1", + "@unhead/vue": "^2.1.4", + "@vueuse/core": "13.9.0", + "fuse.js": "^7.1.0", + "github-slugger": "2.0.0", + "microdiff": "^1.5.0", + "nanoid": "^5.1.6", + "vue": "^3.5.30", + "yaml": "^2.8.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/api-reference/node_modules/nanoid": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/code-highlight": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@scalar/code-highlight/-/code-highlight-0.3.4.tgz", + "integrity": "sha512-gGr3D8bfInwZDHsxamYIaG72Wr+kRNX8d4zcOflAfQJ0ZfvqoVbYhhkiSd6K+DLySItK9lWl/cgjJdtKWlT2ig==", + "license": "MIT", + "dependencies": { + "hast-util-to-text": "^4.0.2", + "highlight.js": "^11.11.1", + "lowlight": "^3.3.0", + "rehype-external-links": "^3.0.0", + "rehype-format": "^5.0.1", + "rehype-parse": "^9.0.1", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/components": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/@scalar/components/-/components-0.22.5.tgz", + "integrity": "sha512-Mjg8mGJdXZN+FTCrIwGvDkqrCMgSr3155dQrQX5AeECUNpfR+aNcoWq/I/i+9ws1Jrkz6pQw0GokCHrOI58ZDw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "0.2.10", + "@floating-ui/vue": "1.1.9", + "@headlessui/vue": "1.7.23", + "@scalar/code-highlight": "0.3.4", + "@scalar/helpers": "0.5.3", + "@scalar/icons": "0.7.2", + "@scalar/themes": "0.15.3", + "@scalar/use-hooks": "0.4.3", + "@vueuse/core": "13.9.0", + "cva": "1.0.0-beta.4", + "nanoid": "^5.1.6", + "radix-vue": "^1.9.17", + "vue": "^3.5.30", + "vue-component-type-helpers": "^3.2.6" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/components/node_modules/nanoid": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-PgQmhuV0oRoHtaqH0OhyCcSY9t35qm8ThNeuUMEAKeN+hW1ijBnJiUADpxaIfXPbLrpN9sjyYza0A16WFbLttg==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/icons": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@scalar/icons/-/icons-0.7.2.tgz", + "integrity": "sha512-21L2y/D6oU7wZHHa9i6FK98cZ+XH4HX9+e69uNpvlp4awRUpz6ifNHOLlxI607bq+Yz4G313gnV0uyUHwZ/pig==", + "license": "MIT", + "dependencies": { + "@phosphor-icons/core": "^2.1.1", + "@types/node": "^24.1.0", + "chalk": "^5.6.2", + "vue": "^3.5.30" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/icons/node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@scalar/json-magic": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/@scalar/json-magic/-/json-magic-0.12.9.tgz", + "integrity": "sha512-wabHE3heo0usLlneDeOjMNs2ES8bREJ3ySc2WPiHIXdzAmy+ERU6g9Al4w3mwgJueOceAkIP6W+yY/DmSCI4uA==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.5.3", + "pathe": "^2.0.3", + "yaml": "^2.8.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/oas-utils": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@scalar/oas-utils/-/oas-utils-0.13.4.tgz", + "integrity": "sha512-kAtxCbs+JMVKmpuvzMVXCxBchvpp/l0VNLvNGtkLAbddccG4aqi0d10TZ8n9IQTdy/AASPa/2p5IHAj9W/frgQ==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.5.3", + "@scalar/themes": "0.15.3", + "@scalar/types": "0.9.3", + "@scalar/workspace-store": "0.47.1", + "flatted": "^3.4.0", + "github-slugger": "2.0.0", + "vue": "^3.5.30", + "yaml": "^2.8.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.8.0.tgz", + "integrity": "sha512-WmaxVSfvY5K/TwcG2B2TU1WOe1As1uc2s7myswtP6dBlcjU3hM08SApxv/jmyGaCE8t4gO5BBhmHY4pDUfmr2g==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-upgrader": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.2.6.tgz", + "integrity": "sha512-pvEmfSCDNYR4+lygidUqfo+shzyp4OSh9+UgK110rzA8Oot6WbJBM03Fuq3M255G7G6R9iXyfsebB7MBUocPkw==", + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.8.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/postman-to-openapi": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@scalar/postman-to-openapi/-/postman-to-openapi-0.7.2.tgz", + "integrity": "sha512-+nZ8xLYuudqmDkY0X2rGX3BGOwshGA/4UpG5KvP5s+H+5cnH1IhTB5QL0nb4doqClPjLN2lBxb3AI53TQXutGQ==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.5.3", + "@scalar/openapi-types": "0.8.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/sidebar": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@scalar/sidebar/-/sidebar-0.9.6.tgz", + "integrity": "sha512-RSzUl1U3eI8QPU2AgGFvv+RWPzTdfKdZyIau4ggjzY2pvaK0MketuybpBV5sqlGnhMesAyUYItEdkBf3gui4VQ==", + "license": "MIT", + "dependencies": { + "@scalar/components": "0.22.5", + "@scalar/helpers": "0.5.3", + "@scalar/icons": "0.7.2", + "@scalar/themes": "0.15.3", + "@scalar/use-hooks": "0.4.3", + "@scalar/workspace-store": "0.47.1", + "vue": "^3.5.30" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/snippetz": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@scalar/snippetz/-/snippetz-0.9.3.tgz", + "integrity": "sha512-y9a/Kyw5DOIv2QxA3KBjKL49px8fS1KPoNf3og7/ok1L3xs26tUh1KsCdPntHnYnMyVdeiuNv0S/4wME7bsTlQ==", + "license": "MIT", + "dependencies": { + "@scalar/types": "0.9.3", + "js-base64": "^3.7.8", + "stringify-object": "^6.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/themes": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@scalar/themes/-/themes-0.15.3.tgz", + "integrity": "sha512-KIGMVglWKxVcdPsdjiXDgyAYhCh53w0qoKRG/cmfP+N4OwR0pk0WzFaMzBscu+sKoZ8SMvZqbXyODO5CBtyD3w==", + "license": "MIT", + "dependencies": { + "nanoid": "^5.1.6" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/themes/node_modules/nanoid": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/typebox": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@scalar/typebox/-/typebox-0.1.3.tgz", + "integrity": "sha512-lU055AUccECZMIfGA0z/C1StYmboAYIPJLDFBzOO81yXBi35Pxdq+I4fWX6iUZ8qcoHneiLGk9jAUM1rA93iEg==", + "license": "MIT" + }, + "node_modules/@scalar/types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.9.3.tgz", + "integrity": "sha512-/cEFjVa8PxRIDyhcWKh7McT8pm5O0kbafzd1jvpVq69sgIIq0gJ0P1sCcPye6qJ2k478PK7VmpK9FxZcr6D4Kw==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.5.3", + "nanoid": "^5.1.6", + "type-fest": "^5.3.1", + "zod": "^4.3.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/types/node_modules/nanoid": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/types/node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@scalar/use-codemirror": { + "version": "0.14.11", + "resolved": "https://registry.npmjs.org/@scalar/use-codemirror/-/use-codemirror-0.14.11.tgz", + "integrity": "sha512-5wtC4pUjzhy72j3aAueJg+fh9KflevJvXMn0YscsxiDTvqwIzeZcHe1N9VNtvzDXgLblEeBT6D0+Vs+boyExxg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.18.3", + "@codemirror/commands": "^6.7.1", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.8", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.10.7", + "@codemirror/lint": "^6.8.4", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.35.3", + "@lezer/common": "^1.2.3", + "@lezer/highlight": "^1.2.1", + "@replit/codemirror-css-color-picker": "^6.3.0", + "vue": "^3.5.30" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/use-hooks": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@scalar/use-hooks/-/use-hooks-0.4.3.tgz", + "integrity": "sha512-dhDWGwqtiVshrAv/bpJ9qPt2Mdbbqyqvtvl2Fau+S9iv7Trsc2XDbfBc40cckSj6EhajgR4EHiuCR0E4DyaveQ==", + "license": "MIT", + "dependencies": { + "@scalar/use-toasts": "0.10.2", + "@vueuse/core": "13.9.0", + "cva": "1.0.0-beta.4", + "tailwind-merge": "3.5.0", + "vue": "^3.5.30", + "zod": "^4.3.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/use-hooks/node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/@scalar/use-toasts": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@scalar/use-toasts/-/use-toasts-0.10.2.tgz", + "integrity": "sha512-1iHQFbDXv0YQRp13aa63S5EcTJ5K8T0ocnLxk+nziloPrLjKt6jdRt6vOHsLSv5sm9kFKcVKNQTQgialmKCOGA==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.30", + "vue-sonner": "^1.3.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/validation": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@scalar/validation/-/validation-0.3.0.tgz", + "integrity": "sha512-4X/AP3JO23DuYxs1MMjn6IlT9gyrKPCuZj8ybTB9QIjC+3tSJLpQOwZg7HEyyz2HoVwOt9jdef2jO3RXW7DqTw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@scalar/workspace-store": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@scalar/workspace-store/-/workspace-store-0.47.1.tgz", + "integrity": "sha512-Qo1jzKQtYwm4kYdTb0HtbdIpsAKtkG19DX/Jy20ZvRk+OshyC0e/YLkHAAEjII89HW8VdpJcIem6fLYbBk1XgQ==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.5.3", + "@scalar/json-magic": "0.12.9", + "@scalar/openapi-upgrader": "0.2.6", + "@scalar/snippetz": "0.9.3", + "@scalar/typebox": "0.1.3", + "@scalar/types": "0.9.3", + "@scalar/validation": "0.3.0", + "github-slugger": "2.0.0", + "js-base64": "^3.7.8", + "type-fest": "^5.3.1", + "vue": "^3.5.30", + "yaml": "^2.8.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/workspace-store/node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@schematics/angular": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.1.tgz", @@ -10908,6 +11781,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -10921,6 +11803,32 @@ "node": ">=10" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz", + "integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.24", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.24.tgz", + "integrity": "sha512-A0k2qF0zFSUStXSZkGXABouXr2Tw2Ztl/cVIYG9qy84uR8W7UNjAcX3DvzBS3YnDcwvLxab8v7dbmYBZ39itDA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.14.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, "node_modules/@timephy/rnnoise-wasm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@timephy/rnnoise-wasm/-/rnnoise-wasm-1.0.0.tgz", @@ -11434,6 +12342,21 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -11680,6 +12603,12 @@ "license": "MIT", "optional": true }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -12310,6 +13239,37 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@unhead/vue": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.13.tgz", + "integrity": "sha512-HYy0shaHRnLNW9r85gppO8IiGz0ONWVV3zGdlT8CQ0tbTwixznJCIiyqV4BSV1aIF1jJIye0pd1p/k6Eab8Z/A==", + "license": "MIT", + "dependencies": { + "hookable": "^6.0.1", + "unhead": "2.1.13" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "vue": ">=3.5.18" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", + "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", @@ -12463,6 +13423,303 @@ "dev": true, "license": "MIT" }, + "node_modules/@vue-macros/common": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", + "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz", + "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.33", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", + "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", + "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.33", + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.10", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-sfc/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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", + "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-kit/node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz", + "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz", + "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", + "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/runtime-core": "3.5.33", + "@vue/shared": "3.5.33", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz", + "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "vue": "3.5.33" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz", + "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz", + "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "13.9.0", + "@vueuse/shared": "13.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.9.0.tgz", + "integrity": "sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "13.9.0", + "@vueuse/shared": "13.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz", + "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz", + "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -12809,6 +14066,24 @@ "node": ">=8" } }, + "node_modules/ai": { + "version": "6.0.33", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.33.tgz", + "integrity": "sha512-bVokbmy2E2QF6Efl+5hOJx5MRWoacZ/CZY/y1E+VcewknvGlgaiCzMu8Xgddz6ArFJjiMFNUPHKxAhIePE4rmg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.13", + "@ai-sdk/provider": "3.0.2", + "@ai-sdk/provider-utils": "4.0.5", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -13262,6 +14537,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -13311,6 +14598,38 @@ "node": ">=12" } }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz", + "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "ast-kit": "^2.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -13654,6 +14973,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -14239,6 +15567,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chardet": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", @@ -14616,6 +15964,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -14958,6 +16316,18 @@ "node": ">= 0.6" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -15476,6 +16846,32 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, + "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==", + "license": "MIT" + }, + "node_modules/cva": { + "version": "1.0.0-beta.4", + "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.4.tgz", + "integrity": "sha512-F/JS9hScapq4DBVQXcK85l9U91M6ePeXoBMSp7vypzShoefUBxjQTo3g3935PUHgQd+IW77DjbPRIxugy4/GCQ==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + }, + "peerDependencies": { + "typescript": ">= 4.5.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cytoscape": { "version": "3.33.1", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", @@ -16227,6 +17623,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -17757,7 +19159,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -17865,6 +19266,12 @@ "express": ">= 4.11" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -18187,11 +19594,20 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "license": "ISC" }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -18671,6 +20087,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function-timeout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", + "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fuse.js": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", + "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -18731,6 +20172,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -18767,6 +20220,12 @@ "dev": true, "license": "MIT" }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -19007,6 +20466,15 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/guess-json-indent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/guess-json-indent/-/guess-json-indent-3.0.1.tgz", + "integrity": "sha512-LWZ3Vr8BG7DHE3TzPYFqkhjNRw4vYgFSsv2nfMuHklAlOfiy54/EwiDQuQfFVLxENCVv20wpbjfTayooQHrEhQ==", + "license": "MIT", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/hachure-fill": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", @@ -19089,6 +20557,339 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/hast-util-from-html/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/hast-util-raw/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -19098,6 +20899,15 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -19120,6 +20930,12 @@ "node": ">=16.9.0" } }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", @@ -19213,6 +21029,26 @@ ], "license": "MIT" }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/htmlparser2": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", @@ -19666,6 +21502,21 @@ "postcss": "^8.1.0" } }, + "node_modules/identifier-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/identifier-regex/-/identifier-regex-1.0.1.tgz", + "integrity": "sha512-ZrYyM0sozNPZlvBvE7Oq9Bn44n0qKGrYu5sQ0JzMUnjIhpgWYE2JB6aBoFwEYdPjqj7jPyxXTMJiHDOxDfd8yw==", + "license": "MIT", + "dependencies": { + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -19842,6 +21693,18 @@ "node": ">= 0.10" } }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -19952,6 +21815,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-identifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-identifier/-/is-identifier-1.0.1.tgz", + "integrity": "sha512-HQ5v4rEJ7REUV54bCd2l5FaD299SGDEn2UPoVXaTHAyGviLq2menVUD2udi3trQ32uvB6LdAh/0ck2EuizrtpA==", + "license": "MIT", + "dependencies": { + "identifier-regex": "^1.0.0", + "super-regex": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -20025,6 +21904,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -20053,6 +21944,18 @@ "dev": true, "license": "MIT" }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -20378,6 +22281,12 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -20424,6 +22333,12 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -21215,6 +23130,40 @@ "node": ">=8.9.0" } }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/local-pkg/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -21401,6 +23350,21 @@ "node": ">=8" } }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -21423,12 +23387,55 @@ "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", - "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/make-asynchronous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", + "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==", + "license": "MIT", + "dependencies": { + "p-event": "^6.0.0", + "type-fest": "^4.6.0", + "web-worker": "^1.5.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-asynchronous/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -21702,6 +23709,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", @@ -21841,6 +23869,12 @@ "node": ">= 0.6" } }, + "node_modules/microdiff": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/microdiff/-/microdiff-1.5.0.tgz", + "integrity": "sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==", + "license": "MIT" + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -22764,6 +24798,109 @@ "ufo": "^1.6.1" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/monaco-editor/node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/monaco-editor/node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/monaco-languageserver-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/monaco-languageserver-types/-/monaco-languageserver-types-0.4.0.tgz", + "integrity": "sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==", + "license": "MIT", + "dependencies": { + "monaco-types": "^0.1.0", + "vscode-languageserver-protocol": "^3.0.0", + "vscode-uri": "^3.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/monaco-marker-data-provider": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.2.5.tgz", + "integrity": "sha512-5ZdcYukhPwgYMCvlZ9H5uWs5jc23BQ8fFF5AhSIdrz5mvYLsqGZ58ZLxTv8rCX6+AxdJ8+vxg1HVSk+F2bLosg==", + "license": "MIT", + "dependencies": { + "monaco-types": "^0.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/monaco-types": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/monaco-types/-/monaco-types-0.1.2.tgz", + "integrity": "sha512-8LwfrlWXsedHwAL41xhXyqzPibS8IqPuIXr9NdORhonS495c2/wky+sI1PRLvMCuiI0nqC2NH1six9hdiRY4Xg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/monaco-worker-manager": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz", + "integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==", + "license": "MIT", + "peerDependencies": { + "monaco-editor": ">=0.30.0" + } + }, + "node_modules/monaco-yaml": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-5.4.1.tgz", + "integrity": "sha512-YQ6d/Ei98Uk073SJLFbwuSi95qhnl8F8NNmIUqN2XhDt9psZN2LqQ1T7pPQ866NJb2wFj44IrjnANgpa2jTfag==", + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "jsonc-parser": "^3.0.0", + "monaco-languageserver-types": "^0.4.0", + "monaco-marker-data-provider": "^1.0.0", + "monaco-types": "^0.1.0", + "monaco-worker-manager": "^2.0.0", + "path-browserify": "^1.0.0", + "prettier": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.0", + "vscode-languageserver-types": "^3.0.0", + "vscode-uri": "^3.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "monaco-editor": ">=0.36" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -22814,6 +24951,12 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -22950,6 +25093,15 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/neverpanic": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/neverpanic/-/neverpanic-0.0.7.tgz", + "integrity": "sha512-GFRTSX2JAEATOCQYlyFkR+9FJPl0pD24toE1foqYAsL6aPLlRKn6L0UFOtJhZCxEbDv+SUsiW4AcPs9cIFwkFw==", + "license": "MIT", + "peerDependencies": { + "typescript": "5" + } + }, "node_modules/ngx-remark": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/ngx-remark/-/ngx-remark-0.2.2.tgz", @@ -23924,6 +26076,21 @@ "node": ">=8" } }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-is-promise": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", @@ -24003,6 +26170,18 @@ "node": ">= 4" } }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -24089,6 +26268,18 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -24296,6 +26487,12 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -25014,9 +27211,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "funding": [ { "type": "opencollective", @@ -25796,7 +27993,6 @@ "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -25847,6 +28043,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -25903,6 +28114,16 @@ "node": ">=10" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -25964,6 +28185,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -25997,6 +28234,140 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/radix-vue": { + "version": "1.9.17", + "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.9.17.tgz", + "integrity": "sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.7", + "@floating-ui/vue": "^1.1.0", + "@internationalized/date": "^3.5.4", + "@internationalized/number": "^3.5.3", + "@tanstack/vue-virtual": "^3.8.1", + "@vueuse/core": "^10.11.0", + "@vueuse/shared": "^10.11.0", + "aria-hidden": "^1.2.4", + "defu": "^6.1.4", + "fast-deep-equal": "^3.1.3", + "nanoid": "^5.0.7" + }, + "peerDependencies": { + "vue": ">= 3.2.0" + } + }, + "node_modules/radix-vue/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/radix-vue/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/radix-vue/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/radix-vue/node_modules/nanoid": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/rambda": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/rambda/-/rambda-9.4.2.tgz", @@ -26226,6 +28597,97 @@ "regjsparser": "bin/parser" } }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sanitize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", @@ -26291,6 +28753,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", @@ -26348,6 +28827,18 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -27224,6 +29715,12 @@ } } }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, "node_modules/secure-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", @@ -27463,6 +29960,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -27920,6 +30423,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spawn-command": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", @@ -28141,6 +30654,24 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-byte-length": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-byte-length/-/string-byte-length-3.0.1.tgz", + "integrity": "sha512-yJ8vP0HMwZ54CcA8S8mKoXbkezpZHANFtmafFo8lGxZThCQcAwRHjdFabuSLgOzxj9OFJcmssmiAvmcOK4O2Hw==", + "license": "MIT", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/string-byte-slice": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-byte-slice/-/string-byte-slice-3.0.1.tgz", + "integrity": "sha512-GWv2K4lYyd2+AhmKH3BV+OVx62xDX+99rSLfKpaqFiQU7uOMaUY1tDjdrRD4gsrCr9lTyjMgjna7tZcCOw+Smg==", + "license": "MIT", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/string-width": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", @@ -28209,6 +30740,38 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-object": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-6.0.0.tgz", + "integrity": "sha512-6f94vIED6vmJJfh3lyVsVWxCYSfI5uM+16ntED/Ql37XIyV6kj0mRAAiTeMMc/QLYIaizC3bUprQ8pQnDDrKfA==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-identifier": "^1.0.1", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -28361,6 +30924,23 @@ "node": ">= 8.0" } }, + "node_modules/super-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz", + "integrity": "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==", + "license": "MIT", + "dependencies": { + "function-timeout": "^1.0.1", + "make-asynchronous": "^1.0.1", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -28447,6 +31027,15 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/swrv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.2.0.tgz", + "integrity": "sha512-lH/g4UcNyj+7lzK4eRGT4C68Q4EhQ6JtM9otPRIASfhhzfLWtbZPHcMuhuba7S9YVYuxkMUGImwMyGpfbkH07A==", + "license": "Apache-2.0", + "peerDependencies": { + "vue": ">=3.2.26 < 4" + } + }, "node_modules/sync-child-process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", @@ -28484,6 +31073,24 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", @@ -28910,6 +31517,21 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "license": "MIT", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -29079,6 +31701,16 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -29089,6 +31721,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/truncate-json": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/truncate-json/-/truncate-json-3.0.1.tgz", + "integrity": "sha512-QVsbr1WhGLq2F0oDyYbqtOXcf3gcnL8C9H5EX8bBwAr8ZWvWGJzukpPrDrWgJMrNtgDbo74BIjI4kJu3q2xQWw==", + "license": "MIT", + "dependencies": { + "guess-json-indent": "^3.0.1", + "string-byte-length": "^3.0.1", + "string-byte-slice": "^3.0.1" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -29941,6 +32587,18 @@ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, + "node_modules/unhead": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.13.tgz", + "integrity": "sha512-jO9M1sI6b2h/1KpIu4Jeu+ptumLmUKboRRLxys5pYHFeT+lqTzfNHbYUX9bxVDhC1FBszAGuWcUVlmvIPsah8Q==", + "license": "MIT", + "dependencies": { + "hookable": "^6.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -30049,6 +32707,20 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -30062,6 +32734,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", @@ -30122,6 +32807,36 @@ "node": ">= 0.8" } }, + "node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/untildify": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz", @@ -30276,6 +32991,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", @@ -30365,6 +33094,15 @@ } } }, + "node_modules/vite-plugin-monaco-editor": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz", + "integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==", + "license": "MIT", + "peerDependencies": { + "monaco-editor": ">=0.33.0" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -30998,6 +33736,121 @@ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "license": "MIT" }, + "node_modules/vue": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", + "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-sfc": "3.5.33", + "@vue/runtime-dom": "3.5.33", + "@vue/server-renderer": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.7.tgz", + "integrity": "sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==", + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz", + "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.6", + "@vue-macros/common": "^3.1.1", + "@vue/devtools-api": "^8.0.6", + "ast-walker-scope": "^0.8.3", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.0", + "muggle-string": "^0.4.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "scule": "^1.3.0", + "tinyglobby": "^0.2.15", + "unplugin": "^3.0.0", + "unplugin-utils": "^0.3.1", + "yaml": "^2.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.17", + "pinia": "^3.0.4", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/vue-router/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/vue-router/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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/vue-router/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/vue-sonner": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-1.3.2.tgz", + "integrity": "sha512-UbZ48E9VIya3ToiRHAZUbodKute/z/M1iT8/3fU8zEbwBRE11AKuHikssv18LMk2gTTr6eMQT4qf6JoLHWuj/A==", + "license": "MIT" + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -31069,6 +33922,22 @@ "license": "MIT", "optional": true }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -31679,6 +34548,12 @@ } } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -32224,7 +35099,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 4809d11..a252568 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,9 @@ "bundle:rnnoise": "esbuild node_modules/@timephy/rnnoise-wasm/dist/NoiseSuppressorWorklet.js --bundle --format=esm --outfile=toju-app/public/rnnoise-worklet.js", "start": "cd \"toju-app\" && ng serve", "build": "cd \"toju-app\" && ng build", + "build:docs": "cd docs-site && npm run build", "build:electron": "tsc -p tsconfig.electron.json", - "build:all": "npm run build && npm run build:electron && cd server && npm run build", + "build:all": "npm run build && npm run build:docs && npm run build:electron && cd server && npm run build", "build:prod": "cd \"toju-app\" && ng build --configuration production --base-href='./'", "watch": "cd \"toju-app\" && ng build --watch --configuration development", "test": "cd \"toju-app\" && vitest run", @@ -29,12 +30,12 @@ "migration:create": "typeorm migration:create electron/migrations/New", "migration:run": "typeorm migration:run -d dist/electron/data-source.js", "migration:revert": "typeorm migration:revert -d dist/electron/data-source.js", - "electron:build": "npm run build:prod && npm run build:electron && electron-builder", - "electron:build:win": "npm run build:prod && npm run build:electron && electron-builder --win", - "electron:build:mac": "npm run build:prod && npm run build:electron && electron-builder --mac", - "electron:build:linux": "npm run build:prod && npm run build:electron && electron-builder --linux", - "electron:build:all": "npm run build:prod && npm run build:electron && electron-builder --win --mac --linux", - "build:prod:all": "npm run build:prod && npm run build:electron && cd server && npm run build", + "electron:build": "npm run build:prod && npm run build:docs && npm run build:electron && electron-builder", + "electron:build:win": "npm run build:prod && npm run build:docs && npm run build:electron && electron-builder --win", + "electron:build:mac": "npm run build:prod && npm run build:docs && npm run build:electron && electron-builder --mac", + "electron:build:linux": "npm run build:prod && npm run build:docs && npm run build:electron && electron-builder --linux", + "electron:build:all": "npm run build:prod && npm run build:docs && npm run build:electron && electron-builder --win --mac --linux", + "build:prod:all": "npm run build:prod && npm run build:docs && npm run build:electron && cd server && npm run build", "build:prod:win": "npm run build:prod:all && electron-builder --win", "dev": "npm run build:electron && npm run electron:full", "dev:app": "npm run electron:dev", @@ -78,6 +79,7 @@ "@ngrx/entity": "^21.0.1", "@ngrx/store": "^21.0.1", "@ngrx/store-devtools": "^21.0.1", + "@scalar/api-reference": "^1.53.1", "@spartan-ng/brain": "^0.0.1-alpha.589", "@spartan-ng/cli": "^0.0.1-alpha.589", "@spartan-ng/ui-core": "^0.0.1-alpha.380", @@ -169,6 +171,17 @@ "filter": [ "**/*" ] + }, + { + "from": "node_modules/@scalar/api-reference/dist/browser/standalone.js", + "to": "scalar/api-reference.js" + }, + { + "from": "docs-site/build", + "to": "docusaurus", + "filter": [ + "**/*" + ] } ], "nodeGypRebuild": false, diff --git a/server/README.md b/server/README.md index ac50250..1ae007f 100644 --- a/server/README.md +++ b/server/README.md @@ -20,6 +20,7 @@ Node/TypeScript signaling server for MetoYou / Toju. This package owns the publi - `SSL` can override the effective HTTP protocol, and `PORT` can override the effective port. - `DB_PATH` can override the SQLite database file location. - `data/variables.json` is normalized on startup and stores `klipyApiKey`, `rawgApiKey`, `releaseManifestUrl`, `serverPort`, `serverProtocol`, `serverHost`, and `linkPreview`. +- `openApiDocs.enabled` in `data/variables.json`, or `OPENAPI_DOCS_ENABLED=true`, exposes the plugin support OpenAPI document at `/api/openapi.json` and a small docs page at `/api/docs`. It is disabled by default. Plugin support is metadata-only: the server stores install requirements and event definitions, but arbitrary plugin data persistence is disabled. - `RAWG_API_KEY` can override `rawgApiKey` for the `/api/games/match` now-playing metadata resolver. Successful matches include a preferred store link from RAWG store metadata, with Steam selected first when available. Negative game-match results are stored in the SQLite `game_match_misses` table so non-game process names do not repeatedly consume RAWG quota. - Packaged server builds store `metoyou.sqlite` in the OS app-data directory by default so upgrades do not overwrite runtime data. On first start, the server copies forward legacy packaged databases that still live beside the executable. - When HTTPS is enabled, certificates are read from the repository `.certs/` directory. diff --git a/server/data/metoyou.sqlite b/server/data/metoyou.sqlite index a9ce375..cf301ec 100644 Binary files a/server/data/metoyou.sqlite and b/server/data/metoyou.sqlite differ diff --git a/server/src/config/variables.ts b/server/src/config/variables.ts index ff95193..401ebb5 100644 --- a/server/src/config/variables.ts +++ b/server/src/config/variables.ts @@ -10,6 +10,10 @@ export interface LinkPreviewConfig { maxCacheSizeMb: number; } +export interface OpenApiDocsConfig { + enabled: boolean; +} + export interface ServerVariablesConfig { klipyApiKey: string; rawgApiKey: string; @@ -18,6 +22,7 @@ export interface ServerVariablesConfig { serverProtocol: ServerHttpProtocol; serverHost: string; linkPreview: LinkPreviewConfig; + openApiDocs: OpenApiDocsConfig; } const DATA_DIR = resolveRuntimePath('data'); @@ -102,6 +107,14 @@ function normalizeLinkPreviewConfig(value: unknown): LinkPreviewConfig { return { enabled, cacheTtlMinutes: cacheTtl, maxCacheSizeMb: maxSize }; } +function normalizeOpenApiDocsConfig(value: unknown): OpenApiDocsConfig { + const raw = (value && typeof value === 'object' && !Array.isArray(value)) + ? value as Record + : {}; + + return { enabled: raw.enabled === true }; +} + function hasEnvironmentOverride(value: string | undefined): value is string { return typeof value === 'string' && value.trim().length > 0; } @@ -149,7 +162,8 @@ export function ensureVariablesConfig(): ServerVariablesConfig { serverPort: normalizeServerPort(remainingParsed.serverPort), serverProtocol: normalizeServerProtocol(remainingParsed.serverProtocol), serverHost: normalizeServerHost(remainingParsed.serverHost ?? legacyServerIpAddress), - linkPreview: normalizeLinkPreviewConfig(remainingParsed.linkPreview) + linkPreview: normalizeLinkPreviewConfig(remainingParsed.linkPreview), + openApiDocs: normalizeOpenApiDocsConfig(remainingParsed.openApiDocs) }; const nextContents = JSON.stringify(normalized, null, 2) + '\n'; @@ -164,7 +178,8 @@ export function ensureVariablesConfig(): ServerVariablesConfig { serverPort: normalized.serverPort, serverProtocol: normalized.serverProtocol, serverHost: normalized.serverHost, - linkPreview: normalized.linkPreview + linkPreview: normalized.linkPreview, + openApiDocs: normalized.openApiDocs }; } @@ -218,6 +233,31 @@ export function isHttpsServerEnabled(): boolean { return getServerProtocol() === 'https'; } +export function areOpenApiDocsEnabled(): boolean { + if (hasEnvironmentOverride(process.env.OPENAPI_DOCS_ENABLED)) { + return process.env.OPENAPI_DOCS_ENABLED.trim().toLowerCase() === 'true'; + } + + return getVariablesConfig().openApiDocs.enabled; +} + +export function setOpenApiDocsEnabled(enabled: boolean): OpenApiDocsConfig { + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + + const { parsed } = readRawVariables(); + const next = { + ...parsed, + openApiDocs: { enabled } + }; + + fs.writeFileSync(VARIABLES_FILE, JSON.stringify(next, null, 2) + '\n', 'utf8'); + ensureVariablesConfig(); + + return { enabled }; +} + export function getLinkPreviewConfig(): LinkPreviewConfig { return getVariablesConfig().linkPreview; } diff --git a/server/src/cqrs/commands/handlers/upsertServer.ts b/server/src/cqrs/commands/handlers/upsertServer.ts index f95c8aa..9a6a697 100644 --- a/server/src/cqrs/commands/handlers/upsertServer.ts +++ b/server/src/cqrs/commands/handlers/upsertServer.ts @@ -18,6 +18,8 @@ export async function handleUpsertServer(command: UpsertServerCommand, dataSourc isPrivate: server.isPrivate ? 1 : 0, maxUsers: server.maxUsers, currentUsers: server.currentUsers, + icon: server.icon ?? null, + iconUpdatedAt: server.iconUpdatedAt ?? 0, slowModeInterval: server.slowModeInterval ?? 0, createdAt: server.createdAt, lastSeen: server.lastSeen diff --git a/server/src/cqrs/mappers.ts b/server/src/cqrs/mappers.ts index 3b4a852..48e6302 100644 --- a/server/src/cqrs/mappers.ts +++ b/server/src/cqrs/mappers.ts @@ -47,6 +47,8 @@ export function rowToServer( isPrivate: !!row.isPrivate, maxUsers: row.maxUsers, currentUsers: row.currentUsers, + icon: row.icon ?? undefined, + iconUpdatedAt: row.iconUpdatedAt || undefined, slowModeInterval: relationPayload.slowModeInterval, tags: relationPayload.tags, channels: relationPayload.channels, diff --git a/server/src/cqrs/types.ts b/server/src/cqrs/types.ts index c1ee5ac..e4f82ef 100644 --- a/server/src/cqrs/types.ts +++ b/server/src/cqrs/types.ts @@ -86,6 +86,8 @@ export interface ServerPayload { isPrivate: boolean; maxUsers: number; currentUsers: number; + icon?: string; + iconUpdatedAt?: number; slowModeInterval?: number; tags: string[]; channels: ServerChannelPayload[]; diff --git a/server/src/db/database.ts b/server/src/db/database.ts index 1babc42..526f210 100644 --- a/server/src/db/database.ts +++ b/server/src/db/database.ts @@ -15,7 +15,12 @@ import { ServerMembershipEntity, ServerInviteEntity, ServerBanEntity, - GameMatchMissEntity + GameMatchMissEntity, + ServerPluginRequirementEntity, + ServerPluginEventDefinitionEntity, + PluginDataEntity, + ServerPluginSettingsEntity, + PluginUserMetadataEntity } from '../entities'; import { serverMigrations } from '../migrations'; import { @@ -49,8 +54,18 @@ const DB_BACKUP = DB_FILE + '.bak'; const DATA_DIR = path.dirname(DB_FILE); // SQLite files start with this 16-byte header string. const SQLITE_MAGIC = 'SQLite format 3\0'; -const SAVE_RETRY_DELAYS_MS = [25, 75, 150, 300, 600]; -const RETRYABLE_SAVE_ERROR_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']); +const SAVE_RETRY_DELAYS_MS = [ + 25, + 75, + 150, + 300, + 600 +]; +const RETRYABLE_SAVE_ERROR_CODES = new Set([ + 'EPERM', + 'EACCES', + 'EBUSY' +]); let applicationDataSource: DataSource | undefined; let saveQueue: Promise = Promise.resolve(); @@ -250,7 +265,12 @@ export async function initDatabase(): Promise { ServerMembershipEntity, ServerInviteEntity, ServerBanEntity, - GameMatchMissEntity + GameMatchMissEntity, + ServerPluginRequirementEntity, + ServerPluginEventDefinitionEntity, + PluginDataEntity, + ServerPluginSettingsEntity, + PluginUserMetadataEntity ], migrations: serverMigrations, synchronize: process.env.DB_SYNCHRONIZE === 'true', diff --git a/server/src/entities/PluginDataEntity.ts b/server/src/entities/PluginDataEntity.ts new file mode 100644 index 0000000..952fe9b --- /dev/null +++ b/server/src/entities/PluginDataEntity.ts @@ -0,0 +1,35 @@ +import { + Column, + Entity, + PrimaryColumn +} from 'typeorm'; + +@Entity('plugin_data') +export class PluginDataEntity { + @PrimaryColumn('text') + serverId!: string; + + @PrimaryColumn('text') + pluginId!: string; + + @PrimaryColumn('text') + scope!: string; + + @PrimaryColumn('text') + ownerId!: string; + + @PrimaryColumn('text') + key!: string; + + @Column('text') + valueJson!: string; + + @Column('integer', { default: 1 }) + schemaVersion!: number; + + @Column('text', { nullable: true }) + updatedBy!: string | null; + + @Column('integer') + updatedAt!: number; +} diff --git a/server/src/entities/PluginUserMetadataEntity.ts b/server/src/entities/PluginUserMetadataEntity.ts new file mode 100644 index 0000000..751a1ba --- /dev/null +++ b/server/src/entities/PluginUserMetadataEntity.ts @@ -0,0 +1,38 @@ +import { + Column, + Entity, + PrimaryColumn +} from 'typeorm'; + +@Entity('plugin_user_metadata') +export class PluginUserMetadataEntity { + @PrimaryColumn('text') + serverId!: string; + + @PrimaryColumn('text') + pluginId!: string; + + @PrimaryColumn('text') + pluginUserId!: string; + + @Column('text') + displayName!: string; + + @Column('text', { nullable: true }) + avatarHash!: string | null; + + @Column('text', { nullable: true }) + avatarMime!: string | null; + + @Column('integer', { nullable: true }) + avatarUpdatedAt!: number | null; + + @Column('text') + roleIdsJson!: string; + + @Column('integer') + createdAt!: number; + + @Column('integer') + updatedAt!: number; +} diff --git a/server/src/entities/ServerEntity.ts b/server/src/entities/ServerEntity.ts index 23665c4..af7b7ed 100644 --- a/server/src/entities/ServerEntity.ts +++ b/server/src/entities/ServerEntity.ts @@ -33,6 +33,12 @@ export class ServerEntity { @Column('integer', { default: 0 }) currentUsers!: number; + @Column('text', { nullable: true }) + icon!: string | null; + + @Column('integer', { default: 0 }) + iconUpdatedAt!: number; + @Column('integer', { default: 0 }) slowModeInterval!: number; diff --git a/server/src/entities/ServerPluginEventDefinitionEntity.ts b/server/src/entities/ServerPluginEventDefinitionEntity.ts new file mode 100644 index 0000000..9b56506 --- /dev/null +++ b/server/src/entities/ServerPluginEventDefinitionEntity.ts @@ -0,0 +1,41 @@ +import { + Column, + Entity, + PrimaryColumn +} from 'typeorm'; + +export type ServerPluginEventDirection = 'clientToServer' | 'serverRelay' | 'p2pHint'; +export type ServerPluginEventScope = 'server' | 'channel' | 'user' | 'plugin'; + +@Entity('server_plugin_event_definitions') +export class ServerPluginEventDefinitionEntity { + @PrimaryColumn('text') + serverId!: string; + + @PrimaryColumn('text') + pluginId!: string; + + @PrimaryColumn('text') + eventName!: string; + + @Column('text') + direction!: ServerPluginEventDirection; + + @Column('text') + scope!: ServerPluginEventScope; + + @Column('text', { nullable: true }) + schemaJson!: string | null; + + @Column('integer') + maxPayloadBytes!: number; + + @Column('text', { nullable: true }) + rateLimitJson!: string | null; + + @Column('integer') + createdAt!: number; + + @Column('integer') + updatedAt!: number; +} diff --git a/server/src/entities/ServerPluginRequirementEntity.ts b/server/src/entities/ServerPluginRequirementEntity.ts new file mode 100644 index 0000000..582ac86 --- /dev/null +++ b/server/src/entities/ServerPluginRequirementEntity.ts @@ -0,0 +1,45 @@ +import { + Column, + Entity, + Index, + PrimaryColumn +} from 'typeorm'; + +export type ServerPluginRequirementStatus = 'required' | 'optional' | 'recommended' | 'blocked' | 'incompatible'; + +@Entity('server_plugin_requirements') +export class ServerPluginRequirementEntity { + @PrimaryColumn('text') + serverId!: string; + + @PrimaryColumn('text') + pluginId!: string; + + @Index() + @Column('text') + status!: ServerPluginRequirementStatus; + + @Column('text', { nullable: true }) + versionRange!: string | null; + + @Column('text', { nullable: true }) + reason!: string | null; + + @Column('text', { nullable: true }) + installUrl!: string | null; + + @Column('text', { nullable: true }) + sourceUrl!: string | null; + + @Column('text', { nullable: true }) + manifestJson!: string | null; + + @Column('text', { nullable: true }) + configuredBy!: string | null; + + @Column('integer') + createdAt!: number; + + @Column('integer') + updatedAt!: number; +} diff --git a/server/src/entities/ServerPluginSettingsEntity.ts b/server/src/entities/ServerPluginSettingsEntity.ts new file mode 100644 index 0000000..4ebc6f9 --- /dev/null +++ b/server/src/entities/ServerPluginSettingsEntity.ts @@ -0,0 +1,26 @@ +import { + Column, + Entity, + PrimaryColumn +} from 'typeorm'; + +@Entity('server_plugin_settings') +export class ServerPluginSettingsEntity { + @PrimaryColumn('text') + serverId!: string; + + @PrimaryColumn('text') + pluginId!: string; + + @Column('text') + settingsJson!: string; + + @Column('integer', { default: 1 }) + schemaVersion!: number; + + @Column('text', { nullable: true }) + updatedBy!: string | null; + + @Column('integer') + updatedAt!: number; +} diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts index b1d2a82..b857605 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -10,3 +10,10 @@ export { ServerMembershipEntity } from './ServerMembershipEntity'; export { ServerInviteEntity } from './ServerInviteEntity'; export { ServerBanEntity } from './ServerBanEntity'; export { GameMatchMissEntity } from './GameMatchMissEntity'; +export { ServerPluginRequirementEntity } from './ServerPluginRequirementEntity'; +export type { ServerPluginRequirementStatus } from './ServerPluginRequirementEntity'; +export { ServerPluginEventDefinitionEntity } from './ServerPluginEventDefinitionEntity'; +export type { ServerPluginEventDirection, ServerPluginEventScope } from './ServerPluginEventDefinitionEntity'; +export { PluginDataEntity } from './PluginDataEntity'; +export { ServerPluginSettingsEntity } from './ServerPluginSettingsEntity'; +export { PluginUserMetadataEntity } from './PluginUserMetadataEntity'; diff --git a/server/src/migrations/1000000000007-PluginSupport.ts b/server/src/migrations/1000000000007-PluginSupport.ts new file mode 100644 index 0000000..747e45a --- /dev/null +++ b/server/src/migrations/1000000000007-PluginSupport.ts @@ -0,0 +1,92 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PluginSupport1000000000007 implements MigrationInterface { + name = 'PluginSupport1000000000007'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "server_plugin_requirements" ( + "serverId" TEXT NOT NULL, + "pluginId" TEXT NOT NULL, + "status" TEXT NOT NULL, + "versionRange" TEXT, + "reason" TEXT, + "configuredBy" TEXT, + "createdAt" INTEGER NOT NULL, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("serverId", "pluginId") + ) + `); + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_server_plugin_requirements_status" + ON "server_plugin_requirements" ("status") + `); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "server_plugin_event_definitions" ( + "serverId" TEXT NOT NULL, + "pluginId" TEXT NOT NULL, + "eventName" TEXT NOT NULL, + "direction" TEXT NOT NULL, + "scope" TEXT NOT NULL, + "schemaJson" TEXT, + "maxPayloadBytes" INTEGER NOT NULL, + "rateLimitJson" TEXT, + "createdAt" INTEGER NOT NULL, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("serverId", "pluginId", "eventName") + ) + `); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "plugin_data" ( + "serverId" TEXT NOT NULL, + "pluginId" TEXT NOT NULL, + "scope" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "valueJson" TEXT NOT NULL, + "schemaVersion" INTEGER NOT NULL DEFAULT 1, + "updatedBy" TEXT, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("serverId", "pluginId", "scope", "ownerId", "key") + ) + `); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "server_plugin_settings" ( + "serverId" TEXT NOT NULL, + "pluginId" TEXT NOT NULL, + "settingsJson" TEXT NOT NULL, + "schemaVersion" INTEGER NOT NULL DEFAULT 1, + "updatedBy" TEXT, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("serverId", "pluginId") + ) + `); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "plugin_user_metadata" ( + "serverId" TEXT NOT NULL, + "pluginId" TEXT NOT NULL, + "pluginUserId" TEXT NOT NULL, + "displayName" TEXT NOT NULL, + "avatarHash" TEXT, + "avatarMime" TEXT, + "avatarUpdatedAt" INTEGER, + "roleIdsJson" TEXT NOT NULL, + "createdAt" INTEGER NOT NULL, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("serverId", "pluginId", "pluginUserId") + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "plugin_user_metadata"`); + await queryRunner.query(`DROP TABLE IF EXISTS "server_plugin_settings"`); + await queryRunner.query(`DROP TABLE IF EXISTS "plugin_data"`); + await queryRunner.query(`DROP TABLE IF EXISTS "server_plugin_event_definitions"`); + await queryRunner.query(`DROP TABLE IF EXISTS "server_plugin_requirements"`); + } +} diff --git a/server/src/migrations/1000000000008-ServerPluginInstallMetadata.ts b/server/src/migrations/1000000000008-ServerPluginInstallMetadata.ts new file mode 100644 index 0000000..ed65eac --- /dev/null +++ b/server/src/migrations/1000000000008-ServerPluginInstallMetadata.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ServerPluginInstallMetadata1000000000008 implements MigrationInterface { + name = 'ServerPluginInstallMetadata1000000000008'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "server_plugin_requirements" ADD COLUMN "installUrl" TEXT`); + await queryRunner.query(`ALTER TABLE "server_plugin_requirements" ADD COLUMN "sourceUrl" TEXT`); + await queryRunner.query(`ALTER TABLE "server_plugin_requirements" ADD COLUMN "manifestJson" TEXT`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_server_plugin_requirements" ( + "serverId" TEXT NOT NULL, + "pluginId" TEXT NOT NULL, + "status" TEXT NOT NULL, + "versionRange" TEXT, + "reason" TEXT, + "configuredBy" TEXT, + "createdAt" INTEGER NOT NULL, + "updatedAt" INTEGER NOT NULL, + PRIMARY KEY ("serverId", "pluginId") + )`); + await queryRunner.query(`INSERT INTO "temporary_server_plugin_requirements" ("serverId", "pluginId", "status", "versionRange", "reason", "configuredBy", "createdAt", "updatedAt") + SELECT "serverId", "pluginId", "status", "versionRange", "reason", "configuredBy", "createdAt", "updatedAt" FROM "server_plugin_requirements"`); + await queryRunner.query(`DROP TABLE "server_plugin_requirements"`); + await queryRunner.query(`ALTER TABLE "temporary_server_plugin_requirements" RENAME TO "server_plugin_requirements"`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_server_plugin_requirements_status" ON "server_plugin_requirements" ("status")`); + } +} diff --git a/server/src/migrations/1000000000009-ServerIcons.ts b/server/src/migrations/1000000000009-ServerIcons.ts new file mode 100644 index 0000000..8d24ed1 --- /dev/null +++ b/server/src/migrations/1000000000009-ServerIcons.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ServerIcons1000000000009 implements MigrationInterface { + name = 'ServerIcons1000000000009'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "servers" ADD COLUMN "icon" TEXT`); + await queryRunner.query(`ALTER TABLE "servers" ADD COLUMN "iconUpdatedAt" INTEGER NOT NULL DEFAULT 0`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "servers_without_icons" ( + "id" TEXT PRIMARY KEY NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "ownerId" TEXT NOT NULL, + "ownerPublicKey" TEXT NOT NULL, + "passwordHash" TEXT, + "isPrivate" INTEGER NOT NULL DEFAULT 0, + "maxUsers" INTEGER NOT NULL DEFAULT 0, + "currentUsers" INTEGER NOT NULL DEFAULT 0, + "slowModeInterval" INTEGER NOT NULL DEFAULT 0, + "createdAt" INTEGER NOT NULL, + "lastSeen" INTEGER NOT NULL + )`); + await queryRunner.query(`INSERT INTO "servers_without_icons" ("id", "name", "description", "ownerId", "ownerPublicKey", "passwordHash", "isPrivate", "maxUsers", "currentUsers", "slowModeInterval", "createdAt", "lastSeen") + SELECT "id", "name", "description", "ownerId", "ownerPublicKey", "passwordHash", "isPrivate", "maxUsers", "currentUsers", "slowModeInterval", "createdAt", "lastSeen" FROM "servers"`); + await queryRunner.query(`DROP TABLE "servers"`); + await queryRunner.query(`ALTER TABLE "servers_without_icons" RENAME TO "servers"`); + } +} diff --git a/server/src/migrations/index.ts b/server/src/migrations/index.ts index 564089b..1545a3d 100644 --- a/server/src/migrations/index.ts +++ b/server/src/migrations/index.ts @@ -5,6 +5,9 @@ import { RepairLegacyVoiceChannels1000000000003 } from './1000000000003-RepairLe import { NormalizeServerArrays1000000000004 } from './1000000000004-NormalizeServerArrays'; import { ServerRoleAccessControl1000000000005 } from './1000000000005-ServerRoleAccessControl'; import { GameMatchMisses1000000000006 } from './1000000000006-GameMatchMisses'; +import { PluginSupport1000000000007 } from './1000000000007-PluginSupport'; +import { ServerPluginInstallMetadata1000000000008 } from './1000000000008-ServerPluginInstallMetadata'; +import { ServerIcons1000000000009 } from './1000000000009-ServerIcons'; export const serverMigrations = [ InitialSchema1000000000000, @@ -13,5 +16,8 @@ export const serverMigrations = [ RepairLegacyVoiceChannels1000000000003, NormalizeServerArrays1000000000004, ServerRoleAccessControl1000000000005, - GameMatchMisses1000000000006 + GameMatchMisses1000000000006, + PluginSupport1000000000007, + ServerPluginInstallMetadata1000000000008, + ServerIcons1000000000009 ]; diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index 874efc1..8c6ba7c 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -6,6 +6,8 @@ import gamesRouter from './games'; import proxyRouter from './proxy'; import usersRouter from './users'; import serversRouter from './servers'; +import pluginSupportRouter from './plugin-support'; +import openApiDocsRouter from './openapi-docs'; import joinRequestsRouter from './join-requests'; import { invitesApiRouter, invitePageRouter } from './invites'; @@ -16,6 +18,8 @@ export function registerRoutes(app: Express): void { app.use('/api/games', gamesRouter); app.use('/api', proxyRouter); app.use('/api/users', usersRouter); + app.use('/api', openApiDocsRouter); + app.use('/api/servers', pluginSupportRouter); app.use('/api/servers', serversRouter); app.use('/api/invites', invitesApiRouter); app.use('/api/requests', joinRequestsRouter); diff --git a/server/src/routes/openapi-docs.ts b/server/src/routes/openapi-docs.ts new file mode 100644 index 0000000..b73e330 --- /dev/null +++ b/server/src/routes/openapi-docs.ts @@ -0,0 +1,106 @@ +import { Router } from 'express'; +import { areOpenApiDocsEnabled, setOpenApiDocsEnabled } from '../config/variables'; + +const router = Router(); + +function createOpenApiDocument(baseUrl: string) { + return { + openapi: '3.1.0', + info: { + title: 'MetoYou Plugin Support API', + version: '1.0.0', + description: 'Official HTTP endpoints for plugin install metadata and event definitions. ' + + 'Plugin code is never executed by the signal server.' + }, + servers: [{ url: `${baseUrl}/api` }], + paths: { + '/servers/{serverId}/plugins': { + get: { + summary: 'Read plugin requirement snapshot', + parameters: [{ name: 'serverId', in: 'path', required: true, schema: { type: 'string' } }], + responses: { '200': { description: 'Plugin requirements and event definitions' } } + } + }, + '/servers/{serverId}/plugins/{pluginId}/requirement': { + put: { + summary: 'Create or update a server plugin requirement', + responses: { '200': { description: 'Requirement saved' }, '403': { description: 'Not authorized' } } + }, + delete: { + summary: 'Delete a server plugin requirement', + responses: { '200': { description: 'Requirement deleted' }, '403': { description: 'Not authorized' } } + } + }, + '/servers/{serverId}/plugins/{pluginId}/events/{eventName}': { + put: { + summary: 'Create or update a plugin event definition', + responses: { '200': { description: 'Event definition saved' }, '403': { description: 'Not authorized' } } + }, + delete: { + summary: 'Delete a plugin event definition', + responses: { '200': { description: 'Event definition deleted' }, '403': { description: 'Not authorized' } } + } + }, + '/servers/{serverId}/plugins/{pluginId}/data': { + get: { + summary: 'Plugin data persistence disabled', + responses: { '410': { description: 'Plugin data persistence is disabled on the signal server' } } + } + }, + '/servers/{serverId}/plugins/{pluginId}/data/{key}': { + put: { + summary: 'Plugin data persistence disabled', + responses: { '410': { description: 'Plugin data persistence is disabled on the signal server' } } + }, + delete: { + summary: 'Plugin data persistence disabled', + responses: { '410': { description: 'Plugin data persistence is disabled on the signal server' } } + } + }, + '/openapi/settings': { + get: { summary: 'Read OpenAPI docs setting', responses: { '200': { description: 'Setting value' } } }, + put: { summary: 'Toggle OpenAPI docs exposure', responses: { '200': { description: 'Setting value' } } } + } + } + }; +} + +function docsDisabledResponse() { + return { error: 'OpenAPI docs are disabled', errorCode: 'OPENAPI_DOCS_DISABLED' }; +} + +router.get('/openapi/settings', (_req, res) => { + res.json({ enabled: areOpenApiDocsEnabled() }); +}); + +router.put('/openapi/settings', (req, res) => { + res.json(setOpenApiDocsEnabled(req.body?.enabled === true)); +}); + +router.get('/openapi.json', (req, res) => { + if (!areOpenApiDocsEnabled()) { + res.status(404).json(docsDisabledResponse()); + return; + } + + res.json(createOpenApiDocument(`${req.protocol}://${req.get('host') ?? 'localhost'}`)); +}); + +router.get('/docs', (_req, res) => { + if (!areOpenApiDocsEnabled()) { + res.status(404).json(docsDisabledResponse()); + return; + } + + res.type('html').send(` + +MetoYou Plugin API Docs + +

MetoYou Plugin Support API

+

Plugin support endpoints are available at /api/openapi.json.

+

The signal server stores plugin install metadata and event definitions only. It never executes plugin code or stores arbitrary plugin data.

+ +`); +}); + +export default router; diff --git a/server/src/routes/plugin-support.ts b/server/src/routes/plugin-support.ts new file mode 100644 index 0000000..4eee6a9 --- /dev/null +++ b/server/src/routes/plugin-support.ts @@ -0,0 +1,148 @@ +import { Response, Router } from 'express'; +import { + deletePluginEventDefinition, + deletePluginRequirement, + getPluginRequirementsSnapshot, + PluginSupportError, + upsertPluginEventDefinition, + upsertPluginRequirement +} from '../services/plugin-support.service'; +import { broadcastToServer } from '../websocket/broadcast'; + +const router = Router(); + +function sendPluginSupportError(error: unknown, res: Response): void { + if (error instanceof PluginSupportError) { + res.status(error.status).json({ error: error.message, errorCode: error.code }); + return; + } + + console.error('Unhandled plugin support error:', error); + res.status(500).json({ error: 'Internal server error', errorCode: 'INTERNAL_ERROR' }); +} + +function readActorUserId(value: unknown): string { + return typeof value === 'string' ? value.trim() : ''; +} + +async function broadcastRequirementsSnapshot(serverId: string): Promise { + const snapshot = await getPluginRequirementsSnapshot(serverId); + + broadcastToServer(serverId, { + type: 'plugin_requirements_changed', + serverId, + snapshot + }); +} + +router.get('/:serverId/plugins', async (req, res) => { + try { + res.json(await getPluginRequirementsSnapshot(req.params.serverId)); + } catch (error) { + sendPluginSupportError(error, res); + } +}); + +router.put('/:serverId/plugins/:pluginId/requirement', async (req, res) => { + const { serverId, pluginId } = req.params; + + try { + const requirement = await upsertPluginRequirement({ + actorUserId: readActorUserId(req.body.actorUserId), + installUrl: req.body.installUrl, + manifest: req.body.manifest, + pluginId, + reason: req.body.reason, + serverId, + sourceUrl: req.body.sourceUrl, + status: req.body.status, + versionRange: req.body.versionRange + }); + + await broadcastRequirementsSnapshot(serverId); + res.json({ requirement }); + } catch (error) { + sendPluginSupportError(error, res); + } +}); + +router.delete('/:serverId/plugins/:pluginId/requirement', async (req, res) => { + const { serverId, pluginId } = req.params; + + try { + await deletePluginRequirement({ + actorUserId: readActorUserId(req.body.actorUserId), + pluginId, + serverId + }); + + await broadcastRequirementsSnapshot(serverId); + res.json({ ok: true }); + } catch (error) { + sendPluginSupportError(error, res); + } +}); + +router.put('/:serverId/plugins/:pluginId/events/:eventName', async (req, res) => { + const { serverId, pluginId, eventName } = req.params; + + try { + const eventDefinition = await upsertPluginEventDefinition({ + actorUserId: readActorUserId(req.body.actorUserId), + direction: req.body.direction, + eventName, + maxPayloadBytes: req.body.maxPayloadBytes, + pluginId, + rateLimitJson: req.body.rateLimitJson, + schemaJson: req.body.schemaJson, + scope: req.body.scope, + serverId + }); + + await broadcastRequirementsSnapshot(serverId); + res.json({ eventDefinition }); + } catch (error) { + sendPluginSupportError(error, res); + } +}); + +router.delete('/:serverId/plugins/:pluginId/events/:eventName', async (req, res) => { + const { serverId, pluginId, eventName } = req.params; + + try { + await deletePluginEventDefinition({ + actorUserId: readActorUserId(req.body.actorUserId), + eventName, + pluginId, + serverId + }); + + await broadcastRequirementsSnapshot(serverId); + res.json({ ok: true }); + } catch (error) { + sendPluginSupportError(error, res); + } +}); + +router.get('/:serverId/plugins/:pluginId/data', (_req, res) => { + res.status(410).json({ + error: 'Plugin data persistence is disabled on the signal server', + errorCode: 'PLUGIN_DATA_DISABLED' + }); +}); + +router.put('/:serverId/plugins/:pluginId/data/:key', (_req, res) => { + res.status(410).json({ + error: 'Plugin data persistence is disabled on the signal server', + errorCode: 'PLUGIN_DATA_DISABLED' + }); +}); + +router.delete('/:serverId/plugins/:pluginId/data/:key', (_req, res) => { + res.status(410).json({ + error: 'Plugin data persistence is disabled on the signal server', + errorCode: 'PLUGIN_DATA_DISABLED' + }); +}); + +export default router; diff --git a/server/src/routes/servers.ts b/server/src/routes/servers.ts index b3318a7..f44edf6 100644 --- a/server/src/routes/servers.ts +++ b/server/src/routes/servers.ts @@ -166,7 +166,9 @@ router.post('/', async (req, res) => { maxUsers, password, tags, - channels + channels, + icon, + iconUpdatedAt } = req.body; if (!name || !ownerId || !ownerPublicKey) @@ -184,6 +186,8 @@ router.post('/', async (req, res) => { isPrivate: isPrivate ?? false, maxUsers: maxUsers ?? 0, currentUsers: 0, + icon: typeof icon === 'string' ? icon : undefined, + iconUpdatedAt: typeof iconUpdatedAt === 'number' ? iconUpdatedAt : undefined, tags: tags ?? [], channels: normalizeServerChannels(channels), createdAt: Date.now(), diff --git a/server/src/services/plugin-support.service.ts b/server/src/services/plugin-support.service.ts new file mode 100644 index 0000000..1c91648 --- /dev/null +++ b/server/src/services/plugin-support.service.ts @@ -0,0 +1,539 @@ +import { getServerById } from '../cqrs'; +import { getDataSource } from '../db/database'; +import { + PluginDataEntity, + ServerPluginEventDefinitionEntity, + ServerPluginEventDirection, + ServerPluginEventScope, + ServerPluginRequirementEntity, + ServerPluginRequirementStatus +} from '../entities'; +import { findServerMembership } from './server-access.service'; +import { resolveServerPermission } from './server-permissions.service'; + +export const DEFAULT_PLUGIN_EVENT_MAX_PAYLOAD_BYTES = 64 * 1024; + +const VALID_REQUIREMENT_STATUSES = new Set([ + 'required', + 'optional', + 'recommended', + 'blocked', + 'incompatible' +]); +const VALID_EVENT_DIRECTIONS = new Set([ + 'clientToServer', + 'serverRelay', + 'p2pHint' +]); +const VALID_EVENT_SCOPES = new Set([ + 'server', + 'channel', + 'user', + 'plugin' +]); +const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]{1,126}[a-z0-9]$/; +const EVENT_NAME_PATTERN = /^[a-z][a-z0-9.:-]{1,126}[a-z0-9]$/; +const DATA_KEY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/; +const DATA_SCOPE_PATTERN = /^[a-zA-Z][a-zA-Z0-9._:-]{0,63}$/; + +export interface PluginRequirementSummary { + installUrl?: string; + manifest?: unknown; + pluginId: string; + reason?: string; + sourceUrl?: string; + status: ServerPluginRequirementStatus; + updatedAt: number; + versionRange?: string; +} + +export interface PluginEventDefinitionSummary { + direction: ServerPluginEventDirection; + eventName: string; + maxPayloadBytes: number; + pluginId: string; + scope: ServerPluginEventScope; + schemaJson?: string; + updatedAt: number; +} + +export interface PluginRequirementsSnapshot { + eventDefinitions: PluginEventDefinitionSummary[]; + requirements: PluginRequirementSummary[]; + serverId: string; + updatedAt: number; +} + +export interface PluginDataRecord { + key: string; + ownerId?: string; + pluginId: string; + schemaVersion: number; + scope: string; + serverId: string; + updatedAt: number; + updatedBy?: string; + value: unknown; +} + +export interface PluginEventEnvelope { + eventId?: string; + eventName: string; + payload: unknown; + pluginId: string; + serverId: string; + sourcePluginUserId?: string; + type: 'plugin_event'; +} + +export class PluginSupportError extends Error { + constructor( + readonly status: number, + readonly code: string, + message: string + ) { + super(message); + this.name = 'PluginSupportError'; + } +} + +function requirementRepository() { + return getDataSource().getRepository(ServerPluginRequirementEntity); +} + +function eventDefinitionRepository() { + return getDataSource().getRepository(ServerPluginEventDefinitionEntity); +} + +function pluginDataRepository() { + return getDataSource().getRepository(PluginDataEntity); +} + +function normalizeOptionalString(value: unknown, maxLength: number): string | null { + if (typeof value !== 'string') { + return null; + } + + const normalized = value.trim(); + + return normalized ? normalized.slice(0, maxLength) : null; +} + +function assertPattern(value: string, pattern: RegExp, code: string, label: string): void { + if (!pattern.test(value)) { + throw new PluginSupportError(400, code, `Invalid ${label}`); + } +} + +function normalizePluginId(pluginId: unknown): string { + const normalized = normalizeOptionalString(pluginId, 128); + + if (!normalized) { + throw new PluginSupportError(400, 'MISSING_PLUGIN_ID', 'Missing plugin id'); + } + + assertPattern(normalized, PLUGIN_ID_PATTERN, 'INVALID_PLUGIN_ID', 'plugin id'); + return normalized; +} + +function normalizeEventName(eventName: unknown): string { + const normalized = normalizeOptionalString(eventName, 128); + + if (!normalized) { + throw new PluginSupportError(400, 'MISSING_EVENT_NAME', 'Missing event name'); + } + + assertPattern(normalized, EVENT_NAME_PATTERN, 'INVALID_EVENT_NAME', 'event name'); + return normalized; +} + +function normalizeDataKey(key: unknown): string { + const normalized = normalizeOptionalString(key, 128); + + if (!normalized) { + throw new PluginSupportError(400, 'MISSING_DATA_KEY', 'Missing data key'); + } + + assertPattern(normalized, DATA_KEY_PATTERN, 'INVALID_DATA_KEY', 'data key'); + return normalized; +} + +function normalizeDataScope(scope: unknown): string { + const normalized = normalizeOptionalString(scope, 64) ?? 'server'; + + assertPattern(normalized, DATA_SCOPE_PATTERN, 'INVALID_DATA_SCOPE', 'data scope'); + return normalized; +} + +function normalizeOwnerId(ownerId: unknown): string { + return normalizeOptionalString(ownerId, 128) ?? ''; +} + +function parseJsonValue(valueJson: string): unknown { + try { + return JSON.parse(valueJson) as unknown; + } catch { + return null; + } +} + +function parseOptionalJsonValue(valueJson: string | null): unknown { + return valueJson ? parseJsonValue(valueJson) : undefined; +} + +function serializeJsonValue(value: unknown, code: string): string { + try { + return JSON.stringify(value ?? null); + } catch { + throw new PluginSupportError(400, code, 'Value must be JSON serializable'); + } +} + +function toRequirementSummary(entity: ServerPluginRequirementEntity): PluginRequirementSummary { + return { + installUrl: entity.installUrl ?? undefined, + manifest: parseOptionalJsonValue(entity.manifestJson), + pluginId: entity.pluginId, + reason: entity.reason ?? undefined, + sourceUrl: entity.sourceUrl ?? undefined, + status: entity.status, + updatedAt: entity.updatedAt, + versionRange: entity.versionRange ?? undefined + }; +} + +function toEventDefinitionSummary(entity: ServerPluginEventDefinitionEntity): PluginEventDefinitionSummary { + return { + direction: entity.direction, + eventName: entity.eventName, + maxPayloadBytes: entity.maxPayloadBytes, + pluginId: entity.pluginId, + scope: entity.scope, + schemaJson: entity.schemaJson ?? undefined, + updatedAt: entity.updatedAt + }; +} + +function toPluginDataRecord(entity: PluginDataEntity): PluginDataRecord { + return { + key: entity.key, + ownerId: entity.ownerId || undefined, + pluginId: entity.pluginId, + schemaVersion: entity.schemaVersion, + scope: entity.scope, + serverId: entity.serverId, + updatedAt: entity.updatedAt, + updatedBy: entity.updatedBy ?? undefined, + value: parseJsonValue(entity.valueJson) + }; +} + +async function assertServerExists(serverId: string) { + const server = await getServerById(serverId); + + if (!server) { + throw new PluginSupportError(404, 'SERVER_NOT_FOUND', 'Server not found'); + } + + return server; +} + +export async function assertCanManagePluginSupport(serverId: string, actorUserId: string): Promise { + const server = await assertServerExists(serverId); + + if (!actorUserId || !resolveServerPermission(server, actorUserId, 'manageServer')) { + throw new PluginSupportError(403, 'NOT_AUTHORIZED', 'Not authorized'); + } +} + +export async function assertCanUsePluginData(serverId: string, actorUserId: string): Promise { + const server = await assertServerExists(serverId); + + if (!actorUserId) { + throw new PluginSupportError(400, 'MISSING_USER', 'Missing user id'); + } + + if (server.ownerId === actorUserId) { + return; + } + + const membership = await findServerMembership(serverId, actorUserId); + + if (!membership) { + throw new PluginSupportError(403, 'NOT_MEMBER', 'Only joined users can access plugin data'); + } +} + +export async function getPluginRequirementsSnapshot(serverId: string): Promise { + await assertServerExists(serverId); + + const requirementQuery = requirementRepository().find({ where: { serverId } }); + const eventDefinitionQuery = eventDefinitionRepository().find({ where: { serverId } }); + const [requirements, eventDefinitions] = await Promise.all([requirementQuery, eventDefinitionQuery]); + const requirementSummaries = requirements + .map(toRequirementSummary) + .sort((first, second) => first.pluginId.localeCompare(second.pluginId)); + const eventDefinitionSummaries = eventDefinitions + .map(toEventDefinitionSummary) + .sort((first, second) => `${first.pluginId}:${first.eventName}`.localeCompare(`${second.pluginId}:${second.eventName}`)); + const updatedAt = Math.max( + 0, + ...requirementSummaries.map((requirement) => requirement.updatedAt), + ...eventDefinitionSummaries.map((definition) => definition.updatedAt) + ); + + return { + eventDefinitions: eventDefinitionSummaries, + requirements: requirementSummaries, + serverId, + updatedAt + }; +} + +export async function upsertPluginRequirement(options: { + actorUserId: string; + installUrl?: unknown; + manifest?: unknown; + pluginId: string; + reason?: unknown; + serverId: string; + sourceUrl?: unknown; + status: unknown; + versionRange?: unknown; +}): Promise { + await assertCanManagePluginSupport(options.serverId, options.actorUserId); + + const pluginId = normalizePluginId(options.pluginId); + const status = options.status; + + if (!VALID_REQUIREMENT_STATUSES.has(status as ServerPluginRequirementStatus)) { + throw new PluginSupportError(400, 'INVALID_REQUIREMENT_STATUS', 'Invalid plugin requirement status'); + } + + const repo = requirementRepository(); + const now = Date.now(); + const existing = await repo.findOne({ where: { serverId: options.serverId, pluginId } }); + const entity = repo.create({ + serverId: options.serverId, + pluginId, + status: status as ServerPluginRequirementStatus, + versionRange: normalizeOptionalString(options.versionRange, 128), + reason: normalizeOptionalString(options.reason, 512), + installUrl: normalizeOptionalString(options.installUrl, 2_000), + sourceUrl: normalizeOptionalString(options.sourceUrl, 2_000), + manifestJson: options.manifest === undefined ? null : serializeJsonValue(options.manifest, 'INVALID_PLUGIN_MANIFEST_METADATA'), + configuredBy: options.actorUserId, + createdAt: existing?.createdAt ?? now, + updatedAt: now + }); + + await repo.save(entity); + return toRequirementSummary(entity); +} + +export async function deletePluginRequirement(options: { + actorUserId: string; + pluginId: string; + serverId: string; +}): Promise { + await assertCanManagePluginSupport(options.serverId, options.actorUserId); + await requirementRepository().delete({ serverId: options.serverId, pluginId: normalizePluginId(options.pluginId) }); +} + +export async function upsertPluginEventDefinition(options: { + actorUserId: string; + direction: unknown; + eventName: string; + maxPayloadBytes?: unknown; + pluginId: string; + rateLimitJson?: unknown; + schemaJson?: unknown; + scope: unknown; + serverId: string; +}): Promise { + await assertCanManagePluginSupport(options.serverId, options.actorUserId); + + const pluginId = normalizePluginId(options.pluginId); + const eventName = normalizeEventName(options.eventName); + const { direction, scope } = options; + + if (!VALID_EVENT_DIRECTIONS.has(direction as ServerPluginEventDirection)) { + throw new PluginSupportError(400, 'INVALID_EVENT_DIRECTION', 'Invalid plugin event direction'); + } + + if (!VALID_EVENT_SCOPES.has(scope as ServerPluginEventScope)) { + throw new PluginSupportError(400, 'INVALID_EVENT_SCOPE', 'Invalid plugin event scope'); + } + + const maxPayloadBytes = typeof options.maxPayloadBytes === 'number' && Number.isFinite(options.maxPayloadBytes) + ? Math.max(1, Math.min(Math.floor(options.maxPayloadBytes), DEFAULT_PLUGIN_EVENT_MAX_PAYLOAD_BYTES)) + : DEFAULT_PLUGIN_EVENT_MAX_PAYLOAD_BYTES; + const repo = eventDefinitionRepository(); + const now = Date.now(); + const existing = await repo.findOne({ where: { serverId: options.serverId, pluginId, eventName } }); + const entity = repo.create({ + serverId: options.serverId, + pluginId, + eventName, + direction: direction as ServerPluginEventDirection, + scope: scope as ServerPluginEventScope, + schemaJson: normalizeOptionalString(options.schemaJson, 10_000), + maxPayloadBytes, + rateLimitJson: normalizeOptionalString(options.rateLimitJson, 2_000), + createdAt: existing?.createdAt ?? now, + updatedAt: now + }); + + await repo.save(entity); + return toEventDefinitionSummary(entity); +} + +export async function deletePluginEventDefinition(options: { + actorUserId: string; + eventName: string; + pluginId: string; + serverId: string; +}): Promise { + await assertCanManagePluginSupport(options.serverId, options.actorUserId); + await eventDefinitionRepository().delete({ + serverId: options.serverId, + pluginId: normalizePluginId(options.pluginId), + eventName: normalizeEventName(options.eventName) + }); +} + +export async function listPluginData(options: { + actorUserId: string; + key?: unknown; + ownerId?: unknown; + pluginId: string; + scope?: unknown; + serverId: string; +}): Promise { + await assertCanUsePluginData(options.serverId, options.actorUserId); + + const pluginId = normalizePluginId(options.pluginId); + const scope = options.scope === undefined ? undefined : normalizeDataScope(options.scope); + const ownerId = options.ownerId === undefined ? undefined : normalizeOwnerId(options.ownerId); + const key = options.key === undefined ? undefined : normalizeDataKey(options.key); + const query = pluginDataRepository() + .createQueryBuilder('data') + .where('data.serverId = :serverId', { serverId: options.serverId }) + .andWhere('data.pluginId = :pluginId', { pluginId }); + + if (scope !== undefined) { + query.andWhere('data.scope = :scope', { scope }); + } + + if (ownerId !== undefined) { + query.andWhere('data.ownerId = :ownerId', { ownerId }); + } + + if (key !== undefined) { + query.andWhere('data.key = :key', { key }); + } + + const records = await query + .orderBy('data.scope', 'ASC') + .addOrderBy('data.ownerId', 'ASC') + .addOrderBy('data.key', 'ASC') + .getMany(); + + return records.map(toPluginDataRecord); +} + +export async function upsertPluginData(options: { + actorUserId: string; + key: string; + ownerId?: unknown; + pluginId: string; + schemaVersion?: unknown; + scope?: unknown; + serverId: string; + value: unknown; +}): Promise { + await assertCanUsePluginData(options.serverId, options.actorUserId); + + const pluginId = normalizePluginId(options.pluginId); + const scope = normalizeDataScope(options.scope); + const ownerId = scope === 'user' ? normalizeOwnerId(options.ownerId ?? options.actorUserId) : normalizeOwnerId(options.ownerId); + + if (scope === 'user' && ownerId !== options.actorUserId) { + await assertCanManagePluginSupport(options.serverId, options.actorUserId); + } + + const key = normalizeDataKey(options.key); + const schemaVersion = typeof options.schemaVersion === 'number' && Number.isFinite(options.schemaVersion) + ? Math.max(1, Math.floor(options.schemaVersion)) + : 1; + const repo = pluginDataRepository(); + const entity = repo.create({ + serverId: options.serverId, + pluginId, + scope, + ownerId, + key, + valueJson: serializeJsonValue(options.value, 'INVALID_PLUGIN_DATA'), + schemaVersion, + updatedBy: options.actorUserId, + updatedAt: Date.now() + }); + + await repo.save(entity); + return toPluginDataRecord(entity); +} + +export async function deletePluginData(options: { + actorUserId: string; + key: string; + ownerId?: unknown; + pluginId: string; + scope?: unknown; + serverId: string; +}): Promise { + await assertCanUsePluginData(options.serverId, options.actorUserId); + + const pluginId = normalizePluginId(options.pluginId); + const scope = normalizeDataScope(options.scope); + const ownerId = scope === 'user' ? normalizeOwnerId(options.ownerId ?? options.actorUserId) : normalizeOwnerId(options.ownerId); + + if (scope === 'user' && ownerId !== options.actorUserId) { + await assertCanManagePluginSupport(options.serverId, options.actorUserId); + } + + await pluginDataRepository().delete({ + serverId: options.serverId, + pluginId, + scope, + ownerId, + key: normalizeDataKey(options.key) + }); +} + +export async function validatePluginEventEnvelope(envelope: PluginEventEnvelope): Promise { + const pluginId = normalizePluginId(envelope.pluginId); + const eventName = normalizeEventName(envelope.eventName); + const definition = await eventDefinitionRepository().findOne({ + where: { + serverId: envelope.serverId, + pluginId, + eventName + } + }); + + if (!definition) { + throw new PluginSupportError(404, 'PLUGIN_EVENT_NOT_REGISTERED', 'Plugin event is not registered for this server'); + } + + if (definition.direction === 'p2pHint') { + throw new PluginSupportError(400, 'PLUGIN_EVENT_NOT_RELAYABLE', 'P2P plugin events must not be relayed by the signal server'); + } + + const payloadBytes = Buffer.byteLength(serializeJsonValue(envelope.payload, 'INVALID_PLUGIN_EVENT_PAYLOAD'), 'utf8'); + + if (payloadBytes > definition.maxPayloadBytes) { + throw new PluginSupportError(413, 'PLUGIN_EVENT_TOO_LARGE', 'Plugin event payload is too large'); + } + + return definition; +} diff --git a/server/src/websocket/handler-plugin.spec.ts b/server/src/websocket/handler-plugin.spec.ts new file mode 100644 index 0000000..247081d --- /dev/null +++ b/server/src/websocket/handler-plugin.spec.ts @@ -0,0 +1,221 @@ +import { + beforeEach, + describe, + expect, + it, + vi +} from 'vitest'; +import { WebSocket } from 'ws'; +import { ConnectedUser } from './types'; +import { connectedUsers } from './state'; + +const pluginSupportMocks = vi.hoisted(() => { + class MockPluginSupportError extends Error { + constructor( + readonly status: number, + readonly code: string, + message: string + ) { + super(message); + this.name = 'PluginSupportError'; + } + } + + return { + getPluginRequirementsSnapshot: vi.fn(), + PluginSupportError: MockPluginSupportError, + validatePluginEventEnvelope: vi.fn() + }; +}); + +vi.mock('../services/server-access.service', () => ({ + authorizeWebSocketJoin: vi.fn(async () => ({ allowed: true as const })) +})); + +vi.mock('../services/plugin-support.service', () => pluginSupportMocks); + +import { handleWebSocketMessage } from './handler'; + +interface SentMessageStore { + sentMessages: string[]; +} + +function createMockWs(): WebSocket & SentMessageStore { + const sentMessages: string[] = []; + const socket = { + readyState: WebSocket.OPEN, + send: (data: string) => { + sentMessages.push(data); + }, + close: () => {}, + sentMessages + } as unknown as WebSocket & SentMessageStore; + + return socket; +} + +function createConnectedUser( + connectionId: string, + oderId: string, + overrides: Partial = {} +): ConnectedUser { + const user: ConnectedUser = { + displayName: `User ${oderId}`, + lastPong: Date.now(), + oderId, + serverIds: new Set(), + ws: createMockWs(), + ...overrides + }; + + connectedUsers.set(connectionId, user); + return user; +} + +function readSentMessages(user: ConnectedUser): Record[] { + return (user.ws as unknown as SentMessageStore).sentMessages.map((messageText) => JSON.parse(messageText) as Record); +} + +describe('server websocket handler - plugin support', () => { + beforeEach(() => { + connectedUsers.clear(); + pluginSupportMocks.getPluginRequirementsSnapshot.mockReset(); + pluginSupportMocks.validatePluginEventEnvelope.mockReset(); + pluginSupportMocks.getPluginRequirementsSnapshot.mockResolvedValue({ + eventDefinitions: [], + requirements: [], + serverId: 'server-1', + updatedAt: 0 + }); + + pluginSupportMocks.validatePluginEventEnvelope.mockResolvedValue({ direction: 'serverRelay' }); + }); + + it('sends plugin requirement snapshots after joining a server', async () => { + const alice = createConnectedUser('conn-1', 'alice'); + + pluginSupportMocks.getPluginRequirementsSnapshot.mockResolvedValue({ + eventDefinitions: [ + { + direction: 'serverRelay', + eventName: 'e2e:relay', + maxPayloadBytes: 2048, + pluginId: 'e2e.plugin-api', + scope: 'server', + updatedAt: 2 + } + ], + requirements: [ + { + pluginId: 'e2e.plugin-api', + status: 'required', + updatedAt: 1 + } + ], + serverId: 'server-1', + updatedAt: 2 + }); + + await handleWebSocketMessage('conn-1', { type: 'join_server', serverId: 'server-1' }); + + const messages = readSentMessages(alice); + const pluginRequirements = messages.find((message) => message['type'] === 'plugin_requirements'); + + expect(pluginRequirements?.['serverId']).toBe('server-1'); + expect(pluginRequirements?.['snapshot']).toEqual(expect.objectContaining({ updatedAt: 2 })); + }); + + it('validates and relays plugin events to other joined users', async () => { + const alice = createConnectedUser('conn-1', 'alice', { viewedServerId: 'server-1' }); + const bob = createConnectedUser('conn-2', 'bob', { viewedServerId: 'server-1' }); + + alice.serverIds.add('server-1'); + bob.serverIds.add('server-1'); + + await handleWebSocketMessage('conn-1', { + type: 'plugin_event', + eventId: 'event-1', + eventName: 'e2e:relay', + payload: { ok: true }, + pluginId: 'e2e.plugin-api', + serverId: 'server-1', + sourcePluginUserId: 'fixture-user' + }); + + expect(pluginSupportMocks.validatePluginEventEnvelope).toHaveBeenCalledWith({ + type: 'plugin_event', + eventId: 'event-1', + eventName: 'e2e:relay', + payload: { ok: true }, + pluginId: 'e2e.plugin-api', + serverId: 'server-1', + sourcePluginUserId: 'fixture-user' + }); + + const bobMessages = readSentMessages(bob); + const relayedEvent = bobMessages.find((message) => message['type'] === 'plugin_event'); + + expect(relayedEvent).toEqual(expect.objectContaining({ + eventId: 'event-1', + eventName: 'e2e:relay', + pluginId: 'e2e.plugin-api', + serverId: 'server-1', + sourcePluginUserId: 'fixture-user', + sourceUserId: 'alice' + })); + + expect(typeof relayedEvent?.['emittedAt']).toBe('number'); + }); + + it('returns plugin errors for invalid plugin event messages', async () => { + const alice = createConnectedUser('conn-1', 'alice'); + + await handleWebSocketMessage('conn-1', { + type: 'plugin_event', + eventName: 'e2e:relay', + pluginId: 'e2e.plugin-api', + serverId: 'server-1' + }); + + const pluginError = readSentMessages(alice).find((message) => message['type'] === 'plugin_error'); + + expect(pluginError).toEqual(expect.objectContaining({ + code: 'INVALID_PLUGIN_EVENT', + eventName: 'e2e:relay', + pluginId: 'e2e.plugin-api', + serverId: 'server-1' + })); + + expect(pluginSupportMocks.validatePluginEventEnvelope).not.toHaveBeenCalled(); + }); + + it('forwards plugin support validation errors to the sending user', async () => { + const alice = createConnectedUser('conn-1', 'alice', { viewedServerId: 'server-1' }); + + alice.serverIds.add('server-1'); + pluginSupportMocks.validatePluginEventEnvelope.mockRejectedValue(new pluginSupportMocks.PluginSupportError( + 400, + 'PLUGIN_EVENT_NOT_RELAYABLE', + 'P2P plugin events must not be relayed by the signal server' + )); + + await handleWebSocketMessage('conn-1', { + type: 'plugin_event', + eventId: 'event-p2p', + eventName: 'e2e:p2p', + payload: { hint: true }, + pluginId: 'e2e.plugin-api', + serverId: 'server-1' + }); + + const pluginError = readSentMessages(alice).find((message) => message['type'] === 'plugin_error'); + + expect(pluginError).toEqual(expect.objectContaining({ + code: 'PLUGIN_EVENT_NOT_RELAYABLE', + eventId: 'event-p2p', + eventName: 'e2e:p2p', + pluginId: 'e2e.plugin-api', + serverId: 'server-1' + })); + }); +}); diff --git a/server/src/websocket/handler.ts b/server/src/websocket/handler.ts index 1d6299e..65a89cd 100644 --- a/server/src/websocket/handler.ts +++ b/server/src/websocket/handler.ts @@ -8,6 +8,11 @@ import { isOderIdConnectedToServer } from './broadcast'; import { authorizeWebSocketJoin } from '../services/server-access.service'; +import { + getPluginRequirementsSnapshot, + PluginSupportError, + validatePluginEventEnvelope +} from '../services/plugin-support.service'; interface WsMessage { [key: string]: unknown; @@ -31,9 +36,7 @@ function normalizeDescription(value: unknown): string | undefined { } function normalizeProfileUpdatedAt(value: unknown): number | undefined { - return typeof value === 'number' && Number.isFinite(value) && value > 0 - ? value - : undefined; + return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined; } function readMessageId(value: unknown): string | undefined { @@ -50,20 +53,62 @@ function readMessageId(value: unknown): string | undefined { return normalized; } +function sendPluginError(user: ConnectedUser, error: unknown, message: WsMessage): void { + if (error instanceof PluginSupportError) { + user.ws.send( + JSON.stringify({ + type: 'plugin_error', + serverId: typeof message['serverId'] === 'string' ? message['serverId'] : undefined, + pluginId: typeof message['pluginId'] === 'string' ? message['pluginId'] : undefined, + eventName: typeof message['eventName'] === 'string' ? message['eventName'] : undefined, + eventId: typeof message['eventId'] === 'string' ? message['eventId'] : undefined, + code: error.code, + message: error.message + }) + ); + + return; + } + + console.error('Unhandled plugin websocket error:', error); + user.ws.send( + JSON.stringify({ + type: 'plugin_error', + code: 'INTERNAL_ERROR', + message: 'Internal server error' + }) + ); +} + /** Sends the current user list for a given server to a single connected user. */ function sendServerUsers(user: ConnectedUser, serverId: string): void { - const users = getUniqueUsersInServer(serverId, user.oderId) - .map(cu => ({ - oderId: cu.oderId, - displayName: normalizeDisplayName(cu.displayName), - description: cu.description, - profileUpdatedAt: cu.profileUpdatedAt, - status: cu.status ?? 'online' - })); + const users = getUniqueUsersInServer(serverId, user.oderId).map((cu) => ({ + oderId: cu.oderId, + displayName: normalizeDisplayName(cu.displayName), + description: cu.description, + profileUpdatedAt: cu.profileUpdatedAt, + status: cu.status ?? 'online' + })); user.ws.send(JSON.stringify({ type: 'server_users', serverId, users })); } +async function sendPluginRequirements(user: ConnectedUser, serverId: string): Promise { + try { + const snapshot = await getPluginRequirementsSnapshot(serverId); + + user.ws.send( + JSON.stringify({ + type: 'plugin_requirements', + serverId, + snapshot + }) + ); + } catch (error) { + sendPluginError(user, error, { type: 'plugin_requirements', serverId }); + } +} + function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: string): void { const newOderId = readMessageId(message['oderId']) ?? connectionId; const newScope = typeof message['connectionScope'] === 'string' ? message['connectionScope'] : undefined; @@ -86,24 +131,24 @@ function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: s connectedUsers.set(connectionId, user); console.log(`User identified: ${user.displayName} (${user.oderId})`); - if ( - user.displayName === previousDisplayName - && user.description === previousDescription - && user.profileUpdatedAt === previousProfileUpdatedAt - ) { + if (user.displayName === previousDisplayName && user.description === previousDescription && user.profileUpdatedAt === previousProfileUpdatedAt) { return; } for (const serverId of user.serverIds) { - broadcastToServer(serverId, { - type: 'user_joined', - oderId: user.oderId, - displayName: normalizeDisplayName(user.displayName), - description: user.description, - profileUpdatedAt: user.profileUpdatedAt, - status: user.status ?? 'online', - serverId - }, user.oderId); + broadcastToServer( + serverId, + { + type: 'user_joined', + oderId: user.oderId, + displayName: normalizeDisplayName(user.displayName), + description: user.description, + profileUpdatedAt: user.profileUpdatedAt, + status: user.status ?? 'online', + serverId + }, + user.oderId + ); } } @@ -116,11 +161,13 @@ async function handleJoinServer(user: ConnectedUser, message: WsMessage, connect const authorization = await authorizeWebSocketJoin(sid, user.oderId); if (!authorization.allowed) { - user.ws.send(JSON.stringify({ - type: 'access_denied', - serverId: sid, - reason: authorization.reason - })); + user.ws.send( + JSON.stringify({ + type: 'access_denied', + serverId: sid, + reason: authorization.reason + }) + ); return; } @@ -132,36 +179,46 @@ async function handleJoinServer(user: ConnectedUser, message: WsMessage, connect user.viewedServerId = sid; connectedUsers.set(connectionId, user); console.log( - `User ${normalizeDisplayName(user.displayName)} (${user.oderId}) joined server ${sid} ` - + `(newConnection=${isNewConnectionMembership}, newIdentity=${isNewIdentityMembership})` + `User ${normalizeDisplayName(user.displayName)} (${user.oderId}) joined server ${sid} ` + + `(newConnection=${isNewConnectionMembership}, newIdentity=${isNewIdentityMembership})` ); sendServerUsers(user, sid); + await sendPluginRequirements(user, sid); if (isNewIdentityMembership) { - broadcastToServer(sid, { - type: 'user_joined', - oderId: user.oderId, - displayName: normalizeDisplayName(user.displayName), - description: user.description, - profileUpdatedAt: user.profileUpdatedAt, - status: user.status ?? 'online', - serverId: sid - }, user.oderId); + broadcastToServer( + sid, + { + type: 'user_joined', + oderId: user.oderId, + displayName: normalizeDisplayName(user.displayName), + description: user.description, + profileUpdatedAt: user.profileUpdatedAt, + status: user.status ?? 'online', + serverId: sid + }, + user.oderId + ); } } -function handleViewServer(user: ConnectedUser, message: WsMessage, connectionId: string): void { +async function handleViewServer(user: ConnectedUser, message: WsMessage, connectionId: string): Promise { const viewSid = readMessageId(message['serverId']); if (!viewSid) return; + if (!user.serverIds.has(viewSid)) { + return; + } + user.viewedServerId = viewSid; connectedUsers.set(connectionId, user); console.log(`User ${normalizeDisplayName(user.displayName)} (${user.oderId}) viewing server ${viewSid}`); sendServerUsers(user, viewSid); + await sendPluginRequirements(user, viewSid); } function handleLeaveServer(user: ConnectedUser, message: WsMessage, connectionId: string): void { @@ -183,13 +240,17 @@ function handleLeaveServer(user: ConnectedUser, message: WsMessage, connectionId return; } - broadcastToServer(leaveSid, { - type: 'user_left', - oderId: user.oderId, - displayName: normalizeDisplayName(user.displayName), - serverId: leaveSid, - serverIds: remainingServerIds - }, user.oderId); + broadcastToServer( + leaveSid, + { + type: 'user_left', + oderId: user.oderId, + displayName: normalizeDisplayName(user.displayName), + serverId: leaveSid, + serverIds: remainingServerIds + }, + user.oderId + ); } function forwardRtcMessage(user: ConnectedUser, message: WsMessage): void { @@ -205,7 +266,7 @@ function forwardRtcMessage(user: ConnectedUser, message: WsMessage): void { } else { console.log( `Target user ${targetUserId} not found. Connected users:`, - Array.from(connectedUsers.values()).map(cu => ({ oderId: cu.oderId, displayName: cu.displayName })) + Array.from(connectedUsers.values()).map((cu) => ({ oderId: cu.oderId, displayName: cu.displayName })) ); } } @@ -227,18 +288,22 @@ function handleChatMessage(user: ConnectedUser, message: WsMessage): void { function handleTyping(user: ConnectedUser, message: WsMessage): void { const typingSid = (message['serverId'] as string | undefined) ?? user.viewedServerId; - const channelId = typeof message['channelId'] === 'string' && message['channelId'].trim() - ? message['channelId'].trim() - : 'general'; + const channelId = typeof message['channelId'] === 'string' && message['channelId'].trim() ? message['channelId'].trim() : 'general'; + const isTyping = message['isTyping'] !== false; if (typingSid && user.serverIds.has(typingSid)) { - broadcastToServer(typingSid, { - type: 'user_typing', - serverId: typingSid, - channelId, - oderId: user.oderId, - displayName: user.displayName - }, user.oderId); + broadcastToServer( + typingSid, + { + type: 'user_typing', + serverId: typingSid, + channelId, + isTyping, + oderId: user.oderId, + displayName: user.displayName + }, + user.oderId + ); } } @@ -260,11 +325,107 @@ function handleStatusUpdate(user: ConnectedUser, message: WsMessage, connectionI console.log(`User ${normalizeDisplayName(user.displayName)} (${user.oderId}) status -> ${status}`); for (const serverId of user.serverIds) { - broadcastToServer(serverId, { - type: 'status_update', - oderId: user.oderId, - status - }, user.oderId); + broadcastToServer( + serverId, + { + type: 'status_update', + oderId: user.oderId, + status + }, + user.oderId + ); + } +} + +function handleServerIconAvailable(user: ConnectedUser, message: WsMessage, connectionId: string): void { + const serverId = readMessageId(message['serverId']); + const iconUpdatedAt = typeof message['iconUpdatedAt'] === 'number' && Number.isFinite(message['iconUpdatedAt']) ? message['iconUpdatedAt'] : 0; + + if (!serverId || iconUpdatedAt <= 0 || !user.serverIds.has(serverId)) { + return; + } + + const availableIcons = user.serverIconUpdatedAtByServerId ?? new Map(); + + availableIcons.set(serverId, iconUpdatedAt); + user.serverIconUpdatedAtByServerId = availableIcons; + connectedUsers.set(connectionId, user); +} + +function handleServerIconSyncRequest(user: ConnectedUser, message: WsMessage): void { + const serverId = readMessageId(message['serverId']); + const localUpdatedAt = typeof message['iconUpdatedAt'] === 'number' && Number.isFinite(message['iconUpdatedAt']) ? message['iconUpdatedAt'] : 0; + + if (!serverId) { + return; + } + + const users = getUniqueUsersInServer(serverId, user.oderId) + .filter((candidate) => (candidate.serverIconUpdatedAtByServerId?.get(serverId) ?? 0) > localUpdatedAt) + .map((candidate) => ({ + oderId: candidate.oderId, + displayName: normalizeDisplayName(candidate.displayName), + description: candidate.description, + profileUpdatedAt: candidate.profileUpdatedAt, + status: candidate.status ?? 'online' + })); + + if (users.length === 0) { + return; + } + + user.ws.send(JSON.stringify({ type: 'server_icon_sync_peers', serverId, users })); +} + +async function handlePluginEvent(user: ConnectedUser, message: WsMessage): Promise { + const serverId = readMessageId(message['serverId']) ?? user.viewedServerId; + const pluginId = readMessageId(message['pluginId']); + const eventName = readMessageId(message['eventName']); + + if (!serverId || !pluginId || !eventName || !user.serverIds.has(serverId)) { + user.ws.send( + JSON.stringify({ + type: 'plugin_error', + serverId, + pluginId, + eventName, + eventId: typeof message['eventId'] === 'string' ? message['eventId'] : undefined, + code: 'INVALID_PLUGIN_EVENT', + message: 'Plugin event is missing required fields or server membership' + }) + ); + + return; + } + + try { + await validatePluginEventEnvelope({ + type: 'plugin_event', + serverId, + pluginId, + eventName, + eventId: typeof message['eventId'] === 'string' ? message['eventId'] : undefined, + payload: message['payload'], + sourcePluginUserId: typeof message['sourcePluginUserId'] === 'string' ? message['sourcePluginUserId'] : undefined + }); + + broadcastToServer( + serverId, + { + type: 'plugin_event', + serverId, + pluginId, + eventName, + eventId: typeof message['eventId'] === 'string' ? message['eventId'] : undefined, + payload: message['payload'], + sourcePluginUserId: typeof message['sourcePluginUserId'] === 'string' ? message['sourcePluginUserId'] : undefined, + sourceUserId: user.oderId, + emittedAt: Date.now() + }, + user.oderId + ); + } catch (error) { + sendPluginError(user, error, message); } } @@ -290,7 +451,7 @@ export async function handleWebSocketMessage(connectionId: string, message: WsMe break; case 'view_server': - handleViewServer(user, message, connectionId); + await handleViewServer(user, message, connectionId); break; case 'leave_server': @@ -300,6 +461,8 @@ export async function handleWebSocketMessage(connectionId: string, message: WsMe case 'offer': case 'answer': case 'ice_candidate': + case 'server_icon_peer_request': + case 'server_icon_peer_data': forwardRtcMessage(user, message); break; @@ -315,6 +478,18 @@ export async function handleWebSocketMessage(connectionId: string, message: WsMe handleStatusUpdate(user, message, connectionId); break; + case 'server_icon_available': + handleServerIconAvailable(user, message, connectionId); + break; + + case 'server_icon_sync_request': + handleServerIconSyncRequest(user, message); + break; + + case 'plugin_event': + await handlePluginEvent(user, message); + break; + default: console.log('Unknown message type:', message.type); } diff --git a/server/src/websocket/types.ts b/server/src/websocket/types.ts index 3e40c66..d6494ef 100644 --- a/server/src/websocket/types.ts +++ b/server/src/websocket/types.ts @@ -17,6 +17,8 @@ export interface ConnectedUser { connectionScope?: string; /** User availability status (online, away, busy, offline). */ status?: 'online' | 'away' | 'busy' | 'offline'; + /** Latest server icon timestamp this connection can provide over P2P. */ + serverIconUpdatedAtByServerId?: Map; /** Timestamp of the last pong or client message received (used to detect dead connections). */ lastPong: number; } diff --git a/toju-app/angular.json b/toju-app/angular.json index 652f152..3bac6d0 100644 --- a/toju-app/angular.json +++ b/toju-app/angular.json @@ -96,13 +96,13 @@ "budgets": [ { "type": "initial", - "maximumWarning": "2.2MB", - "maximumError": "2.38MB" + "maximumWarning": "10mb", + "maximumError": "20mb" }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "10mb", + "maximumError": "20mb" } ], "outputHashing": "all" diff --git a/toju-app/public/plugins/e2e-all-api/README.md b/toju-app/public/plugins/e2e-all-api/README.md new file mode 100644 index 0000000..58a8f81 --- /dev/null +++ b/toju-app/public/plugins/e2e-all-api/README.md @@ -0,0 +1,3 @@ +# E2E All API Plugin + +Fixture plugin for Playwright coverage. It calls every public Toju plugin API surface, registers UI contributions, writes storage, publishes events, creates plugin user data, and logs completion. diff --git a/toju-app/public/plugins/e2e-all-api/icon.svg b/toju-app/public/plugins/e2e-all-api/icon.svg new file mode 100644 index 0000000..3306fe4 --- /dev/null +++ b/toju-app/public/plugins/e2e-all-api/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/toju-app/public/plugins/e2e-all-api/main.js b/toju-app/public/plugins/e2e-all-api/main.js new file mode 100644 index 0000000..1169914 --- /dev/null +++ b/toju-app/public/plugins/e2e-all-api/main.js @@ -0,0 +1,293 @@ +const tinyWave = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA='; +const originalMessage = 'Plugin API original message'; +const editedMessage = 'Plugin API edited message'; +const deletedMessage = 'Plugin API deleted message'; +const embedMessage = 'toju:embed:e2e.coverage:{"title":"Plugin API custom embed","body":"Rendered by plugin API"}'; +const soundboardPlayedMessage = 'E2E soundboard played Airhorn to voice channel'; + +export async function activate(context) { + const api = context.api; + const currentUser = api.profile.getCurrent(); + const shouldMutateChat = !currentUser?.displayName?.includes('Bob'); + const pluginUserId = api.server.registerPluginUser({ + displayName: 'E2E Plugin Bot', + id: 'e2e-plugin-bot' + }); + + context.subscriptions.push(api.ui.registerSettingsPage('coverage', { + label: 'E2E Coverage', + render: () => 'E2E settings contribution' + })); + context.subscriptions.push(api.ui.registerAppPage('coverage', { + label: 'E2E Page', + path: '/plugins/e2e/coverage', + render: () => 'E2E page contribution' + })); + context.subscriptions.push(api.ui.registerSidePanel('coverage', { + label: 'E2E Soundboard', + render: () => 'E2E soundboard ready' + })); + context.subscriptions.push(api.ui.registerChannelSection('coverage', { + label: 'E2E Soundboard', + type: 'custom' + })); + context.subscriptions.push(api.ui.registerComposerAction('coverage', { + icon: 'SFX', + label: 'E2E Soundboard', + run: () => openSoundboardModal(api, pluginUserId) + })); + context.subscriptions.push(api.ui.registerProfileAction('coverage', { + label: 'E2E Profile', + run: () => api.logger.info('profile action ran') + })); + context.subscriptions.push(api.ui.registerToolbarAction('coverage', { + label: 'E2E Toolbar', + run: () => api.logger.info('toolbar action ran') + })); + context.subscriptions.push(api.ui.registerEmbedRenderer('coverage', { + embedType: 'e2e.coverage', + render: (payload) => `E2E custom embed: ${payload?.title ?? 'missing title'}` + })); + + const injectedBadge = document.createElement('div'); + + injectedBadge.dataset.testid = 'e2e-plugin-owned-dom'; + injectedBadge.textContent = 'E2E plugin-owned DOM injected into chat'; + injectedBadge.style.position = 'absolute'; + injectedBadge.style.left = '1rem'; + injectedBadge.style.bottom = '5.5rem'; + injectedBadge.style.zIndex = '20'; + injectedBadge.style.border = '1px solid hsl(var(--border))'; + injectedBadge.style.borderRadius = '0.5rem'; + injectedBadge.style.padding = '0.35rem 0.5rem'; + injectedBadge.style.background = 'hsl(var(--card))'; + injectedBadge.style.color = 'hsl(var(--foreground))'; + injectedBadge.style.fontSize = '0.75rem'; + context.subscriptions.push(api.ui.mountElement('chat-owned-badge', { + element: injectedBadge, + target: 'app-chat-messages' + })); + + context.subscriptions.push(api.events.subscribeServer({ eventName: 'e2e:server', handler: () => {} })); + context.subscriptions.push(api.events.subscribeP2p({ eventName: 'e2e:p2p', handler: () => {} })); + + api.storage.set('coverage', { ok: true }); + api.storage.get('coverage'); + await api.clientData.write('coverage', { ok: true }); + await api.clientData.read('coverage'); + await api.serverData.write('coverage', { ok: true }); + await api.serverData.read('coverage'); + + api.profile.update({ + description: 'Updated by E2E plugin', + displayName: `${currentUser?.displayName || 'E2E Plugin User'} Plugin Renamed` + }); + api.profile.updateAvatar({ + avatarHash: 'e2e-plugin-avatar', + avatarMime: 'image/svg+xml', + avatarUrl: '/plugins/e2e-all-api/icon.svg' + }); + + api.users.getCurrent(); + api.users.list(); + api.users.readMembers(); + api.users.setRole(pluginUserId, 'member'); + api.users.kick(pluginUserId); + api.users.ban(pluginUserId, 'E2E coverage'); + + api.roles.list(); + api.roles.setAssignments([]); + + api.channels.list(); + api.channels.addAudioChannel({ id: 'e2e-audio', name: 'E2E Audio', position: 90 }); + api.channels.addVideoChannel({ id: 'e2e-video', name: 'E2E Video', position: 91 }); + api.channels.select('general'); + api.channels.rename('e2e-audio', 'E2E Audio Renamed'); + + api.server.getCurrent(); + api.server.updatePermissions({ allowVoice: true }); + api.server.updateSettings({ + name: api.server.getCurrent()?.name, + topic: 'Updated by E2E plugin' + }); + + api.messages.readCurrent(); + if (shouldMutateChat) { + const sentMessage = api.messages.send(originalMessage); + + api.messages.edit(sentMessage.id, editedMessage); + + const removableMessage = api.messages.send(deletedMessage); + + api.messages.delete(removableMessage.id); + api.messages.send(embedMessage); + } + + api.messages.sendAsPluginUser({ + content: 'Plugin bot message from all-api fixture', + pluginUserId + }); + api.messages.moderateDelete('missing-message-id'); + api.messages.sync(api.messages.readCurrent()); + context.subscriptions.push(api.messageBus.subscribe({ + handler: () => {}, + latestMessageLimit: 5, + replayLatest: true, + topic: 'e2e:latest' + })); + api.messageBus.publish({ + includeLatestMessages: true, + includeSelf: true, + latestMessageLimit: 5, + payload: { ok: true }, + topic: 'e2e:latest' + }); + api.messageBus.sendLatestMessages({ + limit: 5, + topic: 'e2e:latest' + }); + + api.p2p.connectedPeers(); + api.p2p.broadcastData('e2e:p2p', { ok: true }); + api.p2p.sendData('missing-peer', 'e2e:p2p', { ok: true }); + api.events.publishServer('e2e:server', { ok: true }); + api.events.publishP2p('e2e:p2p', { ok: true }); + + api.media.setOutputVolume(0.8); + api.media.setInputVolume(0.8); + await api.media.playAudioClip({ url: tinyWave, volume: 0 }).catch((error) => api.logger.warn('audio clip rejected', String(error))); + await api.media.addCustomVideoStream({ label: 'e2e-video', stream: new MediaStream() }); + + const audioContext = new AudioContext(); + const destination = audioContext.createMediaStreamDestination(); + + await api.media.addCustomAudioStream({ label: 'e2e-audio', stream: destination.stream }).catch((error) => api.logger.warn('audio stream rejected', String(error))); + await audioContext.close(); + + api.storage.remove('coverage'); + await api.clientData.remove('coverage'); + await api.serverData.remove('coverage'); + api.logger.info('all-api plugin completed'); +} + +export function ready(context) { + context.api.logger.info('all-api plugin ready'); +} + +export function deactivate(context) { + context.api.logger.info('all-api plugin deactivated'); +} + +function openSoundboardModal(api, pluginUserId) { + document.querySelector('[data-testid="e2e-soundboard-modal"]')?.remove(); + + const overlay = document.createElement('div'); + + overlay.dataset.testid = 'e2e-soundboard-modal'; + overlay.setAttribute('role', 'dialog'); + overlay.setAttribute('aria-modal', 'true'); + overlay.setAttribute('aria-label', 'E2E Soundboard'); + overlay.style.position = 'fixed'; + overlay.style.inset = '0'; + overlay.style.zIndex = '9999'; + overlay.style.display = 'grid'; + overlay.style.placeItems = 'center'; + overlay.style.background = 'rgb(0 0 0 / 0.45)'; + + const panel = document.createElement('section'); + + panel.style.width = 'min(24rem, calc(100vw - 2rem))'; + panel.style.border = '1px solid hsl(var(--border))'; + panel.style.borderRadius = '0.5rem'; + panel.style.padding = '1rem'; + panel.style.color = 'hsl(var(--foreground))'; + panel.style.background = 'hsl(var(--card))'; + panel.style.boxShadow = '0 1.25rem 3rem rgb(0 0 0 / 0.25)'; + + const title = document.createElement('h2'); + + title.textContent = 'E2E Soundboard'; + title.style.margin = '0 0 0.75rem'; + title.style.fontSize = '1rem'; + + const status = document.createElement('p'); + + status.dataset.testid = 'e2e-soundboard-status'; + status.textContent = 'Ready to play to voice channel'; + status.style.margin = '0 0 1rem'; + status.style.color = 'hsl(var(--muted-foreground))'; + status.style.fontSize = '0.875rem'; + + const actions = document.createElement('div'); + + actions.style.display = 'flex'; + actions.style.gap = '0.5rem'; + actions.style.justifyContent = 'flex-end'; + + const closeButton = document.createElement('button'); + + closeButton.type = 'button'; + closeButton.textContent = 'Close'; + closeButton.style.border = '1px solid hsl(var(--border))'; + closeButton.style.borderRadius = '0.375rem'; + closeButton.style.padding = '0.5rem 0.75rem'; + closeButton.style.background = 'transparent'; + closeButton.style.color = 'hsl(var(--foreground))'; + closeButton.addEventListener('click', () => overlay.remove()); + + const playButton = document.createElement('button'); + + playButton.type = 'button'; + playButton.textContent = 'Play airhorn to voice'; + playButton.style.border = '0'; + playButton.style.borderRadius = '0.375rem'; + playButton.style.padding = '0.5rem 0.75rem'; + playButton.style.background = 'hsl(var(--primary))'; + playButton.style.color = 'hsl(var(--primary-foreground))'; + playButton.addEventListener('click', async () => { + playButton.disabled = true; + status.textContent = 'Playing Airhorn to voice channel'; + + try { + await playSoundboardClipToVoice(api); + api.p2p.broadcastData('e2e:p2p', { sound: 'airhorn', source: 'soundboard' }); + api.events.publishP2p('e2e:p2p', { sound: 'airhorn', source: 'soundboard' }); + api.messages.sendAsPluginUser({ content: soundboardPlayedMessage, pluginUserId }); + api.logger.info('soundboard played to voice channel'); + status.textContent = soundboardPlayedMessage; + } catch (error) { + status.textContent = error instanceof Error ? error.message : 'Soundboard playback failed'; + api.logger.warn('soundboard playback failed', String(error)); + } finally { + playButton.disabled = false; + } + }); + + actions.append(closeButton, playButton); + panel.append(title, status, actions); + overlay.append(panel); + api.ui.mountElement('soundboard-modal', { + element: overlay, + target: 'body' + }); +} + +async function playSoundboardClipToVoice(api) { + const audioContext = new AudioContext(); + const oscillator = audioContext.createOscillator(); + const gain = audioContext.createGain(); + const destination = audioContext.createMediaStreamDestination(); + + oscillator.type = 'square'; + oscillator.frequency.value = 330; + gain.gain.value = 0.08; + oscillator.connect(gain); + gain.connect(destination); + oscillator.start(); + + await api.media.addCustomAudioStream({ label: 'e2e-soundboard-airhorn', stream: destination.stream }); + await api.media.playAudioClip({ url: tinyWave, volume: 0 }).catch((error) => api.logger.warn('soundboard preview rejected', String(error))); + await new Promise((resolve) => setTimeout(resolve, 150)); + oscillator.stop(); + await audioContext.close(); +} diff --git a/toju-app/public/plugins/e2e-all-api/toju.plugin.json b/toju-app/public/plugins/e2e-all-api/toju.plugin.json new file mode 100644 index 0000000..e4556d2 --- /dev/null +++ b/toju-app/public/plugins/e2e-all-api/toju.plugin.json @@ -0,0 +1,100 @@ +{ + "schemaVersion": 1, + "id": "e2e.all-api-plugin", + "title": "E2E All API Plugin", + "description": "Calls every public Toju plugin API surface for user-facing Playwright coverage.", + "version": "1.0.0", + "kind": "client", + "scope": "server", + "apiVersion": "1.0.0", + "compatibility": { + "minimumTojuVersion": "1.0.0", + "verifiedTojuVersion": "1.0.0" + }, + "entrypoint": "./main.js", + "authors": [ + { + "name": "MetoYou Tests", + "url": "https://git.azaaxin.com/myxelium/Toju" + } + ], + "homepage": "https://git.azaaxin.com/myxelium/Toju", + "readme": "./README.md", + "capabilities": [ + "profile.read", + "profile.write", + "users.read", + "users.manage", + "roles.read", + "roles.manage", + "messages.read", + "messages.send", + "messages.editOwn", + "messages.deleteOwn", + "messages.moderate", + "messages.sync", + "channels.read", + "channels.manage", + "server.read", + "server.manage", + "p2p.data", + "p2p.media", + "media.playAudio", + "media.addAudioStream", + "media.addVideoStream", + "audio.volume", + "audio.effects", + "ui.settings", + "ui.pages", + "ui.sidePanel", + "ui.channelsSection", + "ui.embeds", + "ui.dom", + "storage.local", + "storage.serverData.read", + "storage.serverData.write", + "events.server.publish", + "events.server.subscribe", + "events.p2p.publish", + "events.p2p.subscribe" + ], + "events": [ + { + "eventName": "e2e:server", + "direction": "serverRelay", + "scope": "server", + "maxPayloadBytes": 2048 + }, + { + "eventName": "e2e:p2p", + "direction": "p2pHint", + "scope": "user", + "maxPayloadBytes": 2048 + } + ], + "data": [ + { + "key": "coverage", + "scope": "server", + "storage": "serverData" + } + ], + "settings": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + } + } + }, + "ui": { + "settingsPages": ["coverage"], + "sidePanels": ["coverage"], + "channelSections": ["coverage"] + }, + "pluginUser": { + "displayName": "E2E Plugin Bot", + "label": "All API fixture" + } +} diff --git a/toju-app/public/plugins/e2e-plugin-source.json b/toju-app/public/plugins/e2e-plugin-source.json new file mode 100644 index 0000000..1afe977 --- /dev/null +++ b/toju-app/public/plugins/e2e-plugin-source.json @@ -0,0 +1,18 @@ +{ + "title": "MetoYou E2E Plugin Source", + "plugins": [ + { + "id": "e2e.all-api-plugin", + "title": "E2E All API Plugin", + "description": "Test plugin that calls every public Toju plugin API surface.", + "version": "1.0.0", + "scope": "server", + "author": "MetoYou Tests", + "image": "./e2e-all-api/icon.svg", + "github": "https://git.azaaxin.com/myxelium/Toju", + "homepage": "https://git.azaaxin.com/myxelium/Toju", + "install": "./e2e-all-api/toju.plugin.json", + "readme": "./e2e-all-api/README.md" + } + ] +} diff --git a/toju-app/src/app/app.routes.ts b/toju-app/src/app/app.routes.ts index 3c54a41..9d5ac29 100644 --- a/toju-app/src/app/app.routes.ts +++ b/toju-app/src/app/app.routes.ts @@ -48,5 +48,15 @@ export const routes: Routes = [ path: 'settings', loadComponent: () => import('./features/settings/settings.component').then((module) => module.SettingsComponent) + }, + { + path: 'plugin-store', + loadComponent: () => + import('./domains/plugins/feature/plugin-store/plugin-store.component').then((module) => module.PluginStoreComponent) + }, + { + path: 'plugins/:pluginId/:pageId', + loadComponent: () => + import('./domains/plugins/feature/plugin-page-host/plugin-page-host.component').then((module) => module.PluginPageHostComponent) } ]; diff --git a/toju-app/src/app/app.ts b/toju-app/src/app/app.ts index cc88dc7..60ffdcf 100644 --- a/toju-app/src/app/app.ts +++ b/toju-app/src/app/app.ts @@ -35,6 +35,7 @@ import { SettingsModalService } from './core/services/settings-modal.service'; import { ElectronBridgeService } from './core/platform/electron/electron-bridge.service'; import { UserStatusService } from './core/services/user-status.service'; import { GameActivityService } from './domains/game-activity'; +import { PluginBootstrapService } from './domains/plugins'; import { ServersRailComponent } from './features/servers/servers-rail/servers-rail.component'; import { TitleBarComponent } from './features/shell/title-bar/title-bar.component'; import { FloatingVoiceControlsComponent } from './domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component'; @@ -81,6 +82,7 @@ export class App implements OnInit, OnDestroy { private static readonly THEME_STUDIO_CONTROLS_MARGIN = 16; private static readonly TITLE_BAR_HEIGHT = 40; + readonly plugins = inject(PluginBootstrapService); store = inject(Store); currentRoom = this.store.selectSignal(selectCurrentRoom); desktopUpdates = inject(DesktopAppUpdateService); diff --git a/toju-app/src/app/core/models/index.ts b/toju-app/src/app/core/models/index.ts index 8f58525..663739e 100644 --- a/toju-app/src/app/core/models/index.ts +++ b/toju-app/src/app/core/models/index.ts @@ -49,4 +49,20 @@ export type { ChatAttachmentMeta } from '../../shared-kernel'; +export type { + PluginCapabilityId, + PluginDataChangedMessage, + PluginErrorMessage, + PluginEventDefinitionSummary, + PluginEventDirection, + PluginEventEnvelope, + PluginEventScope, + PluginRequirementStatus, + PluginRequirementSummary, + PluginRequirementsChangedMessage, + PluginRequirementsMessage, + PluginRequirementsSnapshot, + TojuPluginManifest +} from '../../shared-kernel'; + export type { ServerInfo } from '../../domains/server-directory'; diff --git a/toju-app/src/app/core/platform/electron/electron-api.models.ts b/toju-app/src/app/core/platform/electron/electron-api.models.ts index 61cc2cc..8fea11b 100644 --- a/toju-app/src/app/core/platform/electron/electron-api.models.ts +++ b/toju-app/src/app/core/platform/electron/electron-api.models.ts @@ -91,10 +91,12 @@ export interface DesktopSettingsSnapshot { autoStart: boolean; closeToTray: boolean; hardwareAcceleration: boolean; + localApi: LocalApiSettings; manifestUrls: string[]; preferredVersion: string | null; runtimeHardwareAcceleration: boolean; restartRequired: boolean; + vaapiVideoEncode: boolean; } export interface DesktopSettingsPatch { @@ -102,11 +104,34 @@ export interface DesktopSettingsPatch { autoStart?: boolean; closeToTray?: boolean; hardwareAcceleration?: boolean; + localApi?: Partial; manifestUrls?: string[]; preferredVersion?: string | null; vaapiVideoEncode?: boolean; } +export interface LocalApiSettings { + enabled: boolean; + port: number; + exposeOnLan: boolean; + scalarEnabled: boolean; + docusaurusEnabled: boolean; + allowedSignalingServers: string[]; +} + +export type LocalApiStatus = 'stopped' | 'starting' | 'running' | 'error'; + +export interface LocalApiSnapshot { + status: LocalApiStatus; + host: string | null; + port: number | null; + baseUrl: string | null; + error: string | null; + exposeOnLan: boolean; + scalarEnabled: boolean; + docusaurusEnabled: boolean; +} + export interface DesktopNotificationPayload { body: string; requestAttention: boolean; @@ -124,6 +149,28 @@ export interface SavedThemeFileDescriptor { path: string; } +export interface LocalPluginManifestDescriptor { + discoveredAt: number; + entrypointPath?: string; + pluginRootUrl: string; + manifest: unknown; + manifestPath: string; + pluginRoot: string; + readmePath?: string; +} + +export interface LocalPluginDiscoveryError { + manifestPath?: string; + message: string; + pluginRoot?: string; +} + +export interface LocalPluginDiscoveryResult { + errors: LocalPluginDiscoveryError[]; + plugins: LocalPluginManifestDescriptor[]; + pluginsPath: string; +} + export interface ExportUserDataResult { cancelled: boolean; exported: boolean; @@ -189,6 +236,8 @@ export interface ElectronApi { importUserData: () => Promise; eraseUserData: () => Promise; getSavedThemesPath: () => Promise; + getLocalPluginsPath: () => Promise; + listLocalPluginManifests: () => Promise; listSavedThemes: () => Promise; readSavedTheme: (fileName: string) => Promise; writeSavedTheme: (fileName: string, text: string) => Promise; @@ -206,6 +255,9 @@ export interface ElectronApi { restartToApplyUpdate: () => Promise; onAutoUpdateStateChanged: (listener: (state: DesktopUpdateState) => void) => () => void; setDesktopSettings: (patch: DesktopSettingsPatch) => Promise; + getLocalApiStatus: () => Promise; + openLocalApiDocs: () => Promise<{ opened: boolean; reason?: string }>; + openDocusaurusDocs: () => Promise<{ opened: boolean; reason?: string }>; relaunchApp: () => Promise; onDeepLinkReceived: (listener: (url: string) => void) => () => void; readClipboardFiles: () => Promise; diff --git a/toju-app/src/app/core/services/settings-modal.service.ts b/toju-app/src/app/core/services/settings-modal.service.ts index d9b8563..fe73b1d 100644 --- a/toju-app/src/app/core/services/settings-modal.service.ts +++ b/toju-app/src/app/core/services/settings-modal.service.ts @@ -2,14 +2,17 @@ import { Injectable, signal } from '@angular/core'; export type SettingsPage = | 'general' + | 'plugins' | 'theme' | 'network' | 'notifications' | 'voice' | 'updates' + | 'localApi' | 'data' | 'debugging' | 'server' + | 'serverPlugins' | 'members' | 'bans' | 'permissions'; diff --git a/toju-app/src/app/core/storage/current-user-storage.ts b/toju-app/src/app/core/storage/current-user-storage.ts index a6a7777..4d353b2 100644 --- a/toju-app/src/app/core/storage/current-user-storage.ts +++ b/toju-app/src/app/core/storage/current-user-storage.ts @@ -56,4 +56,4 @@ export function clearStoredLocalAppData(): void { localStorage.removeItem(key); } } catch {} -} \ No newline at end of file +} diff --git a/toju-app/src/app/domains/README.md b/toju-app/src/app/domains/README.md index b2e3abf..d43ae82 100644 --- a/toju-app/src/app/domains/README.md +++ b/toju-app/src/app/domains/README.md @@ -15,6 +15,7 @@ infrastructure adapters and UI. | **direct-message** | One-to-one WebRTC messages, offline queueing, delivery state, and friends | `DirectMessageService`, `FriendService` | | **game-activity** | Local game detection, server metadata matching, P2P now-playing sync, and elapsed playtime formatting | `GameActivityService`, `formatGameActivityElapsed()` | | **notifications** | Notification preferences, unread tracking, desktop alert orchestration | `NotificationsFacade` | +| **plugins** | Client-only plugin manifests, load ordering, registry state, and signal-server support metadata | `PluginHostService`, `PluginRegistryService` | | **profile-avatar** | Profile picture upload, crop/zoom editing, processing, local persistence, and P2P avatar sync | `ProfileAvatarFacade` | | **screen-share** | Source picker, quality presets | `ScreenShareFacade` | | **server-directory** | Multi-server endpoint management, health checks, invites, server search UI | `ServerDirectoryFacade` | @@ -32,6 +33,7 @@ The larger domains also keep longer design notes in their own folders: - [chat/README.md](chat/README.md) - [direct-message/README.md](direct-message/README.md) - [notifications/README.md](notifications/README.md) +- [plugins/README.md](plugins/README.md) - [profile-avatar/README.md](profile-avatar/README.md) - [screen-share/README.md](screen-share/README.md) - [server-directory/README.md](server-directory/README.md) diff --git a/toju-app/src/app/domains/authentication/feature/login/login.component.ts b/toju-app/src/app/domains/authentication/feature/login/login.component.ts index 43fa381..a8c4644 100644 --- a/toju-app/src/app/domains/authentication/feature/login/login.component.ts +++ b/toju-app/src/app/domains/authentication/feature/login/login.component.ts @@ -1,4 +1,4 @@ -/* eslint-disable max-statements-per-line */ + import { Component, inject, diff --git a/toju-app/src/app/domains/authentication/feature/register/register.component.ts b/toju-app/src/app/domains/authentication/feature/register/register.component.ts index 1287e86..3761294 100644 --- a/toju-app/src/app/domains/authentication/feature/register/register.component.ts +++ b/toju-app/src/app/domains/authentication/feature/register/register.component.ts @@ -1,4 +1,4 @@ -/* eslint-disable max-statements-per-line */ + import { Component, inject, diff --git a/toju-app/src/app/domains/chat/README.md b/toju-app/src/app/domains/chat/README.md index 0f1f7e4..639c166 100644 --- a/toju-app/src/app/domains/chat/README.md +++ b/toju-app/src/app/domains/chat/README.md @@ -150,4 +150,4 @@ graph LR ## Typing indicator -`TypingIndicatorComponent` listens for typing events from peers scoped to the current server and active text channel. Each event resets a 3-second TTL timer for that channel. If no new event arrives within 3 seconds, the user is removed from the typing list. At most 4 names are shown; beyond that it displays "N users are typing". +`TypingIndicatorComponent` listens for typing events from peers scoped to the current server and active text channel. Each positive event resets a 3-second TTL timer for that channel; an explicit `isTyping: false` event clears that user immediately. If no new event arrives within 3 seconds, the user is removed from the typing list. At most 4 names are shown; beyond that it displays "N users are typing". diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html index 81e0655..cd28e0f 100644 --- a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html +++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html @@ -141,6 +141,20 @@ (drop)="onDrop($event)" >
+ @for (record of pluginComposerActions(); track record.id) { + + } + @if (klipyEnabled()) { . + } @if (msg.linkMetadata?.length) { @@ -115,6 +129,23 @@ } } + @if (pluginEmbeds().length > 0) { +
+ @for (embed of pluginEmbeds(); track embed.id) { +
+
+ {{ embed.contribution.embedType }} + {{ embed.pluginId }} +
+ +
+ } +
+ } + @if (attachmentsList.length > 0) {
@for (att of attachmentsList; track att.id) { diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.ts b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.ts index cc25605..c34c92b 100644 --- a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.ts +++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.ts @@ -12,6 +12,7 @@ import { signal, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { @@ -38,6 +39,8 @@ import { User } from '../../../../../../shared-kernel'; import { ThemeNodeDirective } from '../../../../../theme'; +import { PluginRenderHostComponent } from '../../../../../plugins/feature/plugin-render-host/plugin-render-host.component'; +import { PluginRequirementStateService, PluginUiRegistryService } from '../../../../../plugins'; import { ChatAudioPlayerComponent, @@ -86,6 +89,16 @@ interface ChatMessageAttachmentViewModel extends Attachment { progressPercent: number; } +interface PluginEmbedToken { + embedType: string; + payloadText: string; +} + +interface MissingPluginEmbedFallback { + pluginName: string; + searchTerm: string; +} + @Component({ selector: 'app-chat-message-item', standalone: true, @@ -98,6 +111,7 @@ interface ChatMessageAttachmentViewModel extends Attachment { ChatMessageMarkdownComponent, ChatLinkEmbedComponent, UserAvatarComponent, + PluginRenderHostComponent, ThemeNodeDirective ], viewProviders: [ @@ -124,7 +138,10 @@ export class ChatMessageItemComponent { private readonly attachmentsSvc = inject(AttachmentFacade); private readonly klipy = inject(KlipyService); + private readonly pluginRequirements = inject(PluginRequirementStateService); + private readonly pluginUi = inject(PluginUiRegistryService); private readonly profileCard = inject(ProfileCardService); + private readonly router = inject(Router); private readonly attachmentVersion = signal(this.attachmentsSvc.updated()); readonly message = input.required(); @@ -146,6 +163,9 @@ export class ChatMessageItemComponent { readonly commonEmojis = COMMON_EMOJIS; readonly deletedMessageContent = DELETED_MESSAGE_CONTENT; + readonly pluginEmbedToken = computed(() => parsePluginEmbedToken(this.message().content)); + readonly pluginEmbeds = computed(() => this.findPluginEmbeds(this.pluginEmbedToken())); + readonly missingPluginEmbed = computed(() => this.resolveMissingPluginEmbed()); readonly isEditing = signal(false); readonly showEmojiPicker = signal(false); readonly senderUser = computed(() => { @@ -191,6 +211,52 @@ export class ChatMessageItemComponent { }); }); + openMissingPluginStore(fallback: MissingPluginEmbedFallback): void { + const returnUrl = this.router.url.startsWith('/plugin-store') ? '/search' : this.router.url; + + void this.router.navigate(['/plugin-store'], { + queryParams: { + returnUrl, + search: fallback.searchTerm + } + }); + } + + private findPluginEmbeds(token: PluginEmbedToken | null) { + if (!token) { + return []; + } + + const payload = parseEmbedPayload(token.payloadText); + + return this.pluginUi.embedRecords() + .filter((record) => record.contribution.embedType === token.embedType) + .map((record) => ({ + ...record, + render: () => record.contribution.render(payload) + })); + } + + private resolveMissingPluginEmbed(): MissingPluginEmbedFallback | null { + const token = this.pluginEmbedToken(); + + if (!token || this.pluginEmbeds().length > 0) { + return null; + } + + const missingRequirement = this.pluginRequirements.missingRequiredRequirements() + .find((requirement) => requirement.pluginId === token.embedType || requirement.manifest?.id === token.embedType) + ?? this.pluginRequirements.missingRequiredRequirements() + .find((requirement) => requirement.manifest?.capabilities?.includes('ui.embeds')) + ?? this.pluginRequirements.missingRequiredRequirements()[0]; + const pluginName = missingRequirement?.manifest?.title ?? missingRequirement?.pluginId ?? pluginNameFromEmbedType(token.embedType); + + return { + pluginName, + searchTerm: pluginName + }; + } + startEdit(): void { this.editContent = this.message().content; this.isEditing.set(true); @@ -507,3 +573,38 @@ export class ChatMessageItemComponent { return this.attachmentsSvc.getForMessage(this.message().id).find((attachment) => attachment.id === attachmentId); } } + +function parsePluginEmbedToken(content: string): PluginEmbedToken | null { + const match = /^toju:embed:([a-zA-Z0-9._:-]+):([\s\S]*)$/.exec(content.trim()); + + if (!match) { + return null; + } + + return { + embedType: match[1], + payloadText: match[2] + }; +} + +function pluginNameFromEmbedType(embedType: string): string { + const parts = embedType.split(/[.:_-]/).filter(Boolean); + const pluginParts = parts.length > 2 ? parts.slice(0, -1) : parts; + const label = pluginParts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' ') + .trim(); + + return label || embedType; +} + +function parseEmbedPayload(payloadText: string | undefined): unknown { + if (!payloadText?.trim()) { + return null; + } + + try { + return JSON.parse(payloadText) as unknown; + } catch { + return payloadText; + } +} diff --git a/toju-app/src/app/domains/chat/feature/typing-indicator/typing-indicator.component.ts b/toju-app/src/app/domains/chat/feature/typing-indicator/typing-indicator.component.ts index 37cc6c3..4730198 100644 --- a/toju-app/src/app/domains/chat/feature/typing-indicator/typing-indicator.component.ts +++ b/toju-app/src/app/domains/chat/feature/typing-indicator/typing-indicator.component.ts @@ -25,6 +25,7 @@ const MAX_SHOWN = 4; interface TypingSignalingMessage { type: string; displayName: string; + isTyping?: boolean; oderId: string; serverId: string; channelId?: string; @@ -66,8 +67,14 @@ export class TypingIndicatorComponent { const channelId = typeof msg.channelId === 'string' && msg.channelId.trim() ? msg.channelId.trim() : 'general'; + const typingKey = `${channelId}:${msg.oderId}`; - this.typingMap.set(`${channelId}:${msg.oderId}`, { + if (msg.isTyping === false) { + this.typingMap.delete(typingKey); + return; + } + + this.typingMap.set(typingKey, { name: msg.displayName, channelId, expiresAt: now + TYPING_TTL diff --git a/toju-app/src/app/domains/direct-message/application/services/direct-message.service.spec.ts b/toju-app/src/app/domains/direct-message/application/services/direct-message.service.spec.ts index 1e824b6..469f733 100644 --- a/toju-app/src/app/domains/direct-message/application/services/direct-message.service.spec.ts +++ b/toju-app/src/app/domains/direct-message/application/services/direct-message.service.spec.ts @@ -5,17 +5,13 @@ import { updateMessageStatusInConversation, upsertDirectMessage } from '../../domain/logic/direct-message.logic'; -import type { - DirectMessage, - DirectMessageParticipant -} from '../../domain/models/direct-message.model'; +import type { DirectMessage, DirectMessageParticipant } from '../../domain/models/direct-message.model'; const alice: DirectMessageParticipant = { userId: 'alice', username: 'alice', displayName: 'Alice' }; - const bob: DirectMessageParticipant = { userId: 'bob', username: 'bob', diff --git a/toju-app/src/app/domains/direct-message/application/services/friend.service.ts b/toju-app/src/app/domains/direct-message/application/services/friend.service.ts index 9061e29..541cc77 100644 --- a/toju-app/src/app/domains/direct-message/application/services/friend.service.ts +++ b/toju-app/src/app/domains/direct-message/application/services/friend.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/member-ordering */ import { Injectable, computed, diff --git a/toju-app/src/app/domains/direct-message/feature/friend-button/friend-button.component.ts b/toju-app/src/app/domains/direct-message/feature/friend-button/friend-button.component.ts index c0b7084..856d148 100644 --- a/toju-app/src/app/domains/direct-message/feature/friend-button/friend-button.component.ts +++ b/toju-app/src/app/domains/direct-message/feature/friend-button/friend-button.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/member-ordering */ import { Component, computed, diff --git a/toju-app/src/app/domains/notifications/application/effects/notifications.effects.ts b/toju-app/src/app/domains/notifications/application/effects/notifications.effects.ts index 7d44f25..b946a09 100644 --- a/toju-app/src/app/domains/notifications/application/effects/notifications.effects.ts +++ b/toju-app/src/app/domains/notifications/application/effects/notifications.effects.ts @@ -133,7 +133,7 @@ export class NotificationsEffects { this.notifications.refreshRoomUnreadFromMessages(roomId, roomMessages); } }) - ) - , { dispatch: false } + ), + { dispatch: false } ); } diff --git a/toju-app/src/app/domains/plugins/README.md b/toju-app/src/app/domains/plugins/README.md new file mode 100644 index 0000000..5778d21 --- /dev/null +++ b/toju-app/src/app/domains/plugins/README.md @@ -0,0 +1,29 @@ +# Plugins Domain + +Owns the client-only plugin runtime foundation: manifest validation, deterministic load ordering, registry state, local manifest discovery, capability grants, browser-imported client entrypoints, disposable UI extension registries, plugin logs, and typed access to signal-server plugin support metadata. + +The signal server stores plugin install metadata and event definitions, but it must never execute plugin code or store arbitrary plugin data. Executable plugin loading belongs to the renderer/Electron boundary and should enter this domain through `PluginHostService`. + +Desktop local plugins are discovered from the Electron app data `plugins` folder. Discovery reads `toju-plugin.json` or `plugin.json` from immediate child folders and resolves declared entrypoint/readme paths only when they stay inside that plugin folder. + +The standalone plugin store is available from the title bar Plugins button, the title-bar Plugin Store menu item, the legacy Settings page button, and the Plugin Manager header. It owns source manifest management, search, readmes, install/update/uninstall actions, and links back to installed-plugin management. Manifest `kind` describes runtime shape (`client` or `library`), while top-level manifest `scope` describes installation scope: omit it or use `scope: "client"` for global client plugins, and use `scope: "server"` for chat-server plugins. Server-scoped store entries are presented as Install to Server, Update Server, or Remove from Server. Server plugin downloads are user-local and server-specific: a server can publish requirement metadata, but each account must consent before those plugins are downloaded or activated on join. Members who are already in a server see new required plugin requirements as a blocking prompt with Install plugins or Leave server actions; new optional or recommended requirements appear as a title-bar banner that can be installed, rejected for the current session, or hidden for that server/plugin requirement version. + +The plugin manager UI is split between Settings -> Client plugins for global client plugins and Settings -> Server -> Server plugins for chat-server plugins. The two pages filter by manifest `scope` and include installed plugins, capability grant toggles, per-plugin activate/reload/unload actions, runtime logs, extension-point counts, server requirements, generated settings, and docs. + +The Store tab consumes user-managed HTTP(S), `file://`, or absolute local-path source manifests. Local-path sources and entrypoints are read through the Electron desktop file bridge. A source manifest can expose a `plugins` array whose entries include `id`, `title`, `description`, `version`, `scope`, `author`/`authors`, `image`/`imageUrl`, `github`/`githubUrl`, `install`/`installUrl`/`manifestUrl`, `bundle`/`bundleUrl`, and `readme`/`readmeUrl`. Installing a `scope: "server"` plugin fetches the linked plugin manifest, validates it, registers it with the client registry, and persists the basic install metadata as a server plugin requirement. When a different user joins that server, required plugins block the join until the user accepts the download; optional and recommended plugins are offered as selectable downloads and can be skipped. Once a server has local server-scoped plugins installed, the title bar shows a compact Server plugins button for that server. Installing a `scope: "client"` plugin persists it locally for the current desktop/browser client. + +Store plugins can be published as cached browser bundles by adding `bundle` or `bundleUrl` to the source manifest entry. The bundle is a browser-safe ESM JavaScript file. During install, Electron downloads the bundle into app data under `plugin-bundles///main.js`, writes a cached manifest next to it, and registers the plugin from that local cached manifest path. If no bundle URL is provided and the manifest entrypoint is a relative browser module, Electron caches that entrypoint path instead. Browser-only clients still load directly from the source URL. Saved store sources refresh during app bootstrap; when a source advertises a higher version for an installed plugin, the store attempts to update the local cached bundle and persisted install metadata automatically. + +The server-side plugin support API is metadata-only. The signal server can keep plugin id, requirement status, version range, install/source URLs, and the validated manifest snapshot needed for member clients to install required plugins. Plugin `serverData` API calls are handled as local per-user/per-server client state; HTTP plugin data persistence on the signal server returns `PLUGIN_DATA_DISABLED`. + +Plugin data that belongs to the current client uses the Electron database when the desktop bridge is available. The plugin runtime writes `api.clientData.*` and `api.serverData.*` records to Electron's dedicated user-scoped `plugin_data` table, with renderer localStorage as the browser fallback. The legacy synchronous `api.storage.*` surface remains local and mirrors writes to the same Electron table when possible; plugins that need guaranteed database reads should use the async `api.clientData.*` methods. + +Plugins can communicate over a plugin-only message bus through `api.messageBus`. It sends `plugin-message-bus` data-channel events that are ignored by the normal chat message reducers/effects, can target a peer or broadcast to connected users, and can include a bounded latest-message snapshot filtered by channel, timestamp, and deletion state. + +Plugins can inspect the current interaction context through `api.context.getCurrent()`. Composer action callbacks also receive this context directly, including the local user, current chat server, active text channel, and the user's current voice channel when connected. Plugins with message access can call `api.messages.setTyping(true | false, channelId?)` and can observe peer typing state with `api.messages.subscribeTyping(handler)`, where typing events include the user, server, text channel, and voice channel when those records are available locally. + +Desktop plugin preferences that belong to the local user, including capability grants, disabled plugin ids, and previously activated plugin ids, are persisted through Electron's local database meta table with renderer localStorage as the browser fallback. + +Runtime activation is explicit. `PluginHostService.activateReadyPlugins()` imports browser-safe plugin entrypoints from URL-resolvable manifests, passes a frozen `TojuClientPluginApi`, runs `activate`, then runs `ready` after the load-order pass. Successfully activated plugin ids are remembered locally, and store-installed plugins are reactivated for the active server when their persisted manifests load again. `deactivate` runs during unload/reload, disposables are cleaned in reverse order, and UI contributions are removed by plugin id. + +Plugins that need fully custom UI can call `api.ui.mountElement(id, { target, element, position })` with the `ui.dom` capability. The runtime tags mounted elements with plugin ownership metadata, replaces duplicate mounts for the same plugin/id pair, and removes remaining mounted elements when the plugin is unloaded. diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-bootstrap.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-bootstrap.service.ts new file mode 100644 index 0000000..e4216ec --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-bootstrap.service.ts @@ -0,0 +1,9 @@ +import { Injectable, inject } from '@angular/core'; +import { PluginRequirementStateService } from './plugin-requirement-state.service'; +import { PluginStoreService } from './plugin-store.service'; + +@Injectable({ providedIn: 'root' }) +export class PluginBootstrapService { + readonly requirementState = inject(PluginRequirementStateService); + readonly store = inject(PluginStoreService); +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-capability.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-capability.service.ts new file mode 100644 index 0000000..b7b207e --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-capability.service.ts @@ -0,0 +1,123 @@ +import { + Injectable, + computed, + inject, + signal +} from '@angular/core'; +import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage'; +import type { PluginCapabilityId, TojuPluginManifest } from '../../../../shared-kernel'; +import { PluginDesktopStateService } from './plugin-desktop-state.service'; + +const STORAGE_KEY_PLUGIN_CAPABILITIES = 'metoyou_plugin_capability_grants'; + +export class PluginCapabilityError extends Error { + constructor(pluginId: string, capability: PluginCapabilityId) { + super(`Plugin ${pluginId} needs capability ${capability}`); + this.name = 'PluginCapabilityError'; + } +} + +@Injectable({ providedIn: 'root' }) +export class PluginCapabilityService { + readonly grants = computed(() => this.grantsSignal()); + + private readonly desktopState = inject(PluginDesktopStateService); + private readonly grantsSignal = signal>(this.loadGrants()); + + constructor() { + void this.loadDesktopGrants(); + } + + grant(pluginId: string, capability: PluginCapabilityId): void { + this.grantsSignal.update((grants) => ({ + ...grants, + [pluginId]: Array.from(new Set([...(grants[pluginId] ?? []), capability])).sort() + })); + + void this.saveGrants(); + } + + grantAll(manifest: TojuPluginManifest): void { + this.grantsSignal.update((grants) => ({ + ...grants, + [manifest.id]: [...(manifest.capabilities ?? [])].sort() + })); + + void this.saveGrants(); + } + + revoke(pluginId: string, capability: PluginCapabilityId): void { + this.grantsSignal.update((grants) => ({ + ...grants, + [pluginId]: (grants[pluginId] ?? []).filter((entry) => entry !== capability) + })); + + void this.saveGrants(); + } + + revokeAll(pluginId: string): void { + this.grantsSignal.update((grants) => { + const { [pluginId]: _removed, ...next } = grants; + + return next; + }); + + void this.saveGrants(); + } + + has(pluginId: string, capability: PluginCapabilityId): boolean { + return this.grants()[pluginId]?.includes(capability) ?? false; + } + + assert(pluginId: string, capability: PluginCapabilityId): void { + if (!this.has(pluginId, capability)) { + throw new PluginCapabilityError(pluginId, capability); + } + } + + missing(manifest: TojuPluginManifest): PluginCapabilityId[] { + return (manifest.capabilities ?? []).filter((capability) => !this.has(manifest.id, capability)); + } + + private loadGrants(): Record { + try { + const raw = localStorage.getItem(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_CAPABILITIES)); + + if (!raw) { + return {}; + } + + const parsed = JSON.parse(raw) as unknown; + + return isGrantRecord(parsed) ? parsed : {}; + } catch { + return {}; + } + } + + private async loadDesktopGrants(): Promise { + const grants = await this.desktopState.readJson>(STORAGE_KEY_PLUGIN_CAPABILITIES, this.grantsSignal()); + + if (isGrantRecord(grants)) { + this.grantsSignal.set(grants); + } + } + + private async saveGrants(): Promise { + try { + localStorage.setItem( + getUserScopedStorageKey(STORAGE_KEY_PLUGIN_CAPABILITIES), + JSON.stringify(this.grantsSignal()) + ); + } catch {} + + await this.desktopState.writeJson(STORAGE_KEY_PLUGIN_CAPABILITIES, this.grantsSignal()); + } +} + +function isGrantRecord(value: unknown): value is Record { + return !!value + && typeof value === 'object' + && !Array.isArray(value) + && Object.values(value).every((entry) => Array.isArray(entry) && entry.every((item) => typeof item === 'string')); +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-client-api.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-client-api.service.ts new file mode 100644 index 0000000..3a65d12 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-client-api.service.ts @@ -0,0 +1,718 @@ +import { Injectable, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Subscription } from 'rxjs'; +import { RealtimeSessionFacade } from '../../../../core/realtime'; +import { DatabaseService } from '../../../../infrastructure/persistence'; +import { VoiceConnectionFacade } from '../../../voice-connection/application/facades/voice-connection.facade'; +import type { + Channel, + ChatEvent, + Message, + PluginCapabilityId, + PluginEventEnvelope, + TojuPluginManifest, + User +} from '../../../../shared-kernel'; +import { MessagesActions } from '../../../../store/messages/messages.actions'; +import { selectCurrentRoomMessages } from '../../../../store/messages/messages.selectors'; +import { RoomsActions } from '../../../../store/rooms/rooms.actions'; +import { + selectActiveChannelId, + selectCurrentRoom, + selectCurrentRoomChannels, + selectCurrentRoomId +} from '../../../../store/rooms/rooms.selectors'; +import { UsersActions } from '../../../../store/users/users.actions'; +import { selectAllUsers, selectCurrentUser } from '../../../../store/users/users.selectors'; +import type { + PluginApiAvatarUpdate, + PluginApiActionContext, + PluginApiActionSource, + PluginApiChannelRequest, + PluginApiCustomStreamRequest, + PluginApiMessageAsPluginUserRequest, + PluginApiServerSettingsUpdate, + PluginApiTypingEvent, + TojuClientPluginApi, + TojuPluginDisposable +} from '../../domain/models/plugin-api.models'; +import { PluginCapabilityService } from './plugin-capability.service'; +import { PluginLoggerService } from './plugin-logger.service'; +import { PluginMessageBusService } from './plugin-message-bus.service'; +import { PluginStorageService } from './plugin-storage.service'; +import { PluginUiRegistryService } from './plugin-ui-registry.service'; + +@Injectable({ providedIn: 'root' }) +export class PluginClientApiService { + private readonly capabilities = inject(PluginCapabilityService); + private readonly db = inject(DatabaseService); + private readonly logger = inject(PluginLoggerService); + private readonly messageBus = inject(PluginMessageBusService); + private readonly realtime = inject(RealtimeSessionFacade); + private readonly store = inject(Store); + private readonly storage = inject(PluginStorageService); + private readonly uiRegistry = inject(PluginUiRegistryService); + private readonly voice = inject(VoiceConnectionFacade); + + private readonly currentMessages = this.store.selectSignal(selectCurrentRoomMessages); + private readonly currentRoom = this.store.selectSignal(selectCurrentRoom); + private readonly currentRoomChannels = this.store.selectSignal(selectCurrentRoomChannels); + private readonly currentRoomId = this.store.selectSignal(selectCurrentRoomId); + private readonly activeChannelId = this.store.selectSignal(selectActiveChannelId); + private readonly currentUser = this.store.selectSignal(selectCurrentUser); + private readonly users = this.store.selectSignal(selectAllUsers); + + createApi(manifest: TojuPluginManifest): TojuClientPluginApi { + const pluginId = manifest.id; + const requireCapability = (capability: PluginCapabilityId): void => this.capabilities.assert(pluginId, capability); + const assertEvent = (eventName: string): void => this.assertDeclaredEvent(manifest, eventName); + + return deepFreeze({ + channels: { + addAudioChannel: (request) => { + requireCapability('channels.manage'); + this.store.dispatch(RoomsActions.addChannel({ channel: createChannel(request, 'voice') })); + }, + addVideoChannel: (request) => { + requireCapability('channels.manage'); + this.uiRegistry.registerChannelSection(pluginId, request.id ?? request.name, { + label: request.name, + order: request.position, + type: 'video' + }); + }, + list: () => { + requireCapability('channels.read'); + return this.currentRoomChannels(); + }, + remove: (channelId) => { + requireCapability('channels.manage'); + this.store.dispatch(RoomsActions.removeChannel({ channelId })); + }, + rename: (channelId, name) => { + requireCapability('channels.manage'); + this.store.dispatch(RoomsActions.renameChannel({ channelId, name })); + }, + select: (channelId) => { + requireCapability('channels.read'); + this.store.dispatch(RoomsActions.selectChannel({ channelId })); + } + }, + events: { + publishP2p: (eventName, payload) => { + requireCapability('events.p2p.publish'); + assertEvent(eventName); + this.broadcastPluginEvent(pluginId, eventName, payload, 'p2p'); + }, + publishServer: (eventName, payload) => { + requireCapability('events.server.publish'); + assertEvent(eventName); + this.publishServerPluginEvent(pluginId, eventName, payload); + }, + subscribeP2p: (subscription) => { + requireCapability('events.p2p.subscribe'); + assertEvent(subscription.eventName); + return this.rememberSubscription(pluginId, subscription.eventName); + }, + subscribeServer: (subscription) => { + requireCapability('events.server.subscribe'); + assertEvent(subscription.eventName); + return this.subscribeServerPluginEvent(pluginId, subscription.eventName, subscription.handler); + } + }, + logger: { + debug: (message, data) => this.logger.debug(pluginId, message, data), + error: (message, data) => this.logger.error(pluginId, message, data), + info: (message, data) => this.logger.info(pluginId, message, data), + warn: (message, data) => this.logger.warn(pluginId, message, data) + }, + context: { + getCurrent: () => this.createActionContext('manual') + }, + clientData: { + read: async (key) => { + requireCapability('storage.local'); + return await this.storage.readClientData(pluginId, key); + }, + remove: async (key) => { + requireCapability('storage.local'); + await this.storage.removeClientData(pluginId, key); + }, + write: async (key, value) => { + requireCapability('storage.local'); + await this.storage.writeClientData(pluginId, key, value); + } + }, + media: { + addCustomAudioStream: async (request) => { + requireCapability('media.addAudioStream'); + await this.voice.setLocalStream(request.stream); + }, + addCustomVideoStream: async (_request: PluginApiCustomStreamRequest) => { + requireCapability('media.addVideoStream'); + this.logger.info(pluginId, 'Video stream contribution registered'); + }, + playAudioClip: async (request) => { + requireCapability('media.playAudio'); + await playAudioClip(request.url, request.volume); + }, + setInputVolume: (volume) => { + requireCapability('audio.volume'); + this.voice.setInputVolume(volume); + }, + setOutputVolume: (volume) => { + requireCapability('audio.volume'); + this.voice.setOutputVolume(volume); + } + }, + messages: { + delete: (messageId) => { + requireCapability('messages.deleteOwn'); + this.deletePluginMessage(pluginId, messageId); + }, + edit: (messageId, content) => { + requireCapability('messages.editOwn'); + this.editPluginMessage(pluginId, messageId, content); + }, + moderateDelete: (messageId) => { + requireCapability('messages.moderate'); + this.store.dispatch(MessagesActions.adminDeleteMessage({ messageId })); + }, + readCurrent: () => { + requireCapability('messages.read'); + return this.currentMessages(); + }, + send: (content, channelId) => { + requireCapability('messages.send'); + return this.sendPluginMessage(pluginId, content, channelId); + }, + sendAsPluginUser: (request) => { + requireCapability('messages.send'); + this.receivePluginUserMessage(pluginId, request); + }, + setTyping: (isTyping, channelId) => { + requireCapability('messages.send'); + this.setTyping(pluginId, isTyping, channelId); + }, + subscribeTyping: (handler) => { + requireCapability('messages.read'); + return this.subscribeTyping(pluginId, handler); + }, + sync: (messages) => { + requireCapability('messages.sync'); + this.store.dispatch(MessagesActions.syncMessages({ messages })); + } + }, + messageBus: { + publish: (request) => { + requireCapability('events.p2p.publish'); + + if (request.includeLatestMessages) { + requireCapability('messages.read'); + } + + return this.messageBus.publish(pluginId, request); + }, + sendLatestMessages: (request = {}) => { + requireCapability('events.p2p.publish'); + requireCapability('messages.read'); + return this.messageBus.sendLatestMessages(pluginId, request); + }, + subscribe: (subscription) => { + requireCapability('events.p2p.subscribe'); + + if (subscription.replayLatest) { + requireCapability('messages.read'); + } + + return this.messageBus.subscribe(pluginId, subscription); + } + }, + p2p: { + broadcastData: (eventName, payload) => { + requireCapability('p2p.data'); + this.broadcastPluginEvent(pluginId, eventName, payload, 'p2p'); + }, + connectedPeers: () => { + requireCapability('p2p.data'); + return this.voice.getConnectedPeers(); + }, + sendData: (peerId, eventName, payload) => { + requireCapability('p2p.data'); + this.broadcastPluginEvent(pluginId, eventName, { payload, peerId }, 'p2p'); + } + }, + profile: { + getCurrent: () => { + requireCapability('profile.read'); + return this.currentUser() ?? null; + }, + update: (profile) => { + requireCapability('profile.write'); + this.store.dispatch(UsersActions.updateCurrentUserProfile({ + profile: { + ...profile, + profileUpdatedAt: Date.now() + } + })); + }, + updateAvatar: (avatar: PluginApiAvatarUpdate) => { + requireCapability('profile.write'); + this.store.dispatch(UsersActions.updateCurrentUserAvatar({ + avatar: { + ...avatar, + avatarUpdatedAt: Date.now() + } + })); + } + }, + roles: { + list: () => { + requireCapability('roles.read'); + return this.currentRoom()?.roles ?? []; + }, + setAssignments: (assignments) => { + requireCapability('roles.manage'); + this.updateRoomAccessControl({ roleAssignments: assignments }); + } + }, + server: { + getCurrent: () => { + requireCapability('server.read'); + return this.currentRoom(); + }, + registerPluginUser: (request) => { + requireCapability('users.manage'); + const userId = request.id ?? `${pluginId}:${slug(request.displayName)}`; + + this.store.dispatch(UsersActions.userJoined({ + user: { + avatarUrl: request.avatarUrl, + displayName: request.displayName, + id: userId, + isOnline: true, + joinedAt: Date.now(), + oderId: userId, + role: 'member', + status: 'online', + username: userId + } + })); + + return userId; + }, + updatePermissions: (permissions) => { + requireCapability('server.manage'); + this.store.dispatch(RoomsActions.updateRoomPermissions({ roomId: this.requireRoomId(), permissions })); + }, + updateSettings: (settings: PluginApiServerSettingsUpdate) => { + requireCapability('server.manage'); + this.store.dispatch(RoomsActions.updateRoomSettings({ + roomId: this.requireRoomId(), + settings: { + description: settings.description, + hasPassword: !!settings.password, + isPrivate: settings.isPrivate ?? this.currentRoom()?.isPrivate ?? false, + maxUsers: settings.maxUsers, + name: settings.name ?? this.currentRoom()?.name ?? 'Server', + password: settings.password, + rules: [], + topic: settings.topic + } + })); + } + }, + serverData: { + read: async (key) => { + requireCapability('storage.serverData.read'); + return await this.storage.readServerData(pluginId, key); + }, + remove: async (key) => { + requireCapability('storage.serverData.write'); + await this.storage.removeServerData(pluginId, key); + }, + write: async (key, value) => { + requireCapability('storage.serverData.write'); + await this.storage.writeServerData(pluginId, key, value); + } + }, + storage: { + get: (key) => { + requireCapability('storage.local'); + return this.storage.getLocal(pluginId, key); + }, + remove: (key) => { + requireCapability('storage.local'); + this.storage.removeLocal(pluginId, key); + }, + set: (key, value) => { + requireCapability('storage.local'); + this.storage.setLocal(pluginId, key, value); + } + }, + ui: { + registerAppPage: (id, contribution) => { + requireCapability('ui.pages'); + return this.uiRegistry.registerAppPage(pluginId, id, contribution); + }, + registerChannelSection: (id, contribution) => { + requireCapability('ui.channelsSection'); + return this.uiRegistry.registerChannelSection(pluginId, id, contribution); + }, + registerComposerAction: (id, contribution) => { + requireCapability('ui.pages'); + return this.uiRegistry.registerComposerAction(pluginId, id, contribution); + }, + registerEmbedRenderer: (id, contribution) => { + requireCapability('ui.embeds'); + return this.uiRegistry.registerEmbedRenderer(pluginId, id, contribution); + }, + mountElement: (id, request) => { + requireCapability('ui.dom'); + return this.uiRegistry.mountElement(pluginId, id, request); + }, + registerProfileAction: (id, contribution) => { + requireCapability('ui.pages'); + return this.uiRegistry.registerProfileAction(pluginId, id, contribution); + }, + registerSettingsPage: (id, contribution) => { + requireCapability('ui.settings'); + return this.uiRegistry.registerSettingsPage(pluginId, id, contribution); + }, + registerSidePanel: (id, contribution) => { + requireCapability('ui.sidePanel'); + return this.uiRegistry.registerSidePanel(pluginId, id, contribution); + }, + registerToolbarAction: (id, contribution) => { + requireCapability('ui.pages'); + return this.uiRegistry.registerToolbarAction(pluginId, id, contribution); + } + }, + users: { + ban: (userId, reason) => { + requireCapability('users.manage'); + this.store.dispatch(UsersActions.banUser({ reason, userId })); + }, + getCurrent: () => { + requireCapability('users.read'); + return this.currentUser() ?? null; + }, + kick: (userId) => { + requireCapability('users.manage'); + this.store.dispatch(UsersActions.kickUser({ userId })); + }, + list: () => { + requireCapability('users.read'); + return this.users(); + }, + readMembers: () => { + requireCapability('users.read'); + return this.currentRoom()?.members ?? []; + }, + setRole: (userId, role: User['role']) => { + requireCapability('roles.manage'); + this.store.dispatch(UsersActions.updateUserRole({ role, userId })); + } + } + }); + } + + createActionContext(source: PluginApiActionSource): PluginApiActionContext { + const user = this.currentUser() ?? null; + const server = this.currentRoom(); + const channels = this.currentRoomChannels(); + const activeChannelId = this.activeChannelId() ?? 'general'; + const voiceChannelId = user?.voiceState?.roomId ?? null; + + return { + server, + source, + textChannel: channels.find((channel) => channel.type === 'text' && channel.id === activeChannelId) ?? null, + user, + voiceChannel: voiceChannelId + ? channels.find((channel) => channel.type === 'voice' && channel.id === voiceChannelId) ?? null + : null + }; + } + + private assertDeclaredEvent(manifest: TojuPluginManifest, eventName: string): void { + const declared = manifest.events?.some((event) => event.eventName === eventName) ?? false; + + if (!declared) { + throw new Error(`Plugin ${manifest.id} did not declare event ${eventName}`); + } + } + + private broadcastPluginEvent(pluginId: string, eventName: string, payload: unknown, target: 'p2p' | 'server'): void { + const roomId = this.currentRoomId() ?? 'local'; + const event: PluginEventEnvelope = { + emittedAt: Date.now(), + eventId: createId(), + eventName, + payload, + pluginId, + serverId: roomId, + type: 'plugin_event' + }; + + this.voice.broadcastMessage({ + data: JSON.stringify({ event, target }), + roomId, + timestamp: Date.now(), + type: 'plugin-event' + } as unknown as ChatEvent); + } + + private publishServerPluginEvent(pluginId: string, eventName: string, payload: unknown): void { + this.realtime.sendRawMessage({ + type: 'plugin_event', + eventId: createId(), + eventName, + payload, + pluginId, + serverId: this.requireRoomId() + }); + } + + private subscribeServerPluginEvent( + pluginId: string, + eventName: string, + handler: (event: PluginEventEnvelope) => void + ) { + const subscription = new Subscription(); + + subscription.add(this.realtime.onSignalingMessage.subscribe((message) => { + const record = message as Record; + + if (record['type'] !== 'plugin_event' || record['pluginId'] !== pluginId || record['eventName'] !== eventName) { + return; + } + + handler(message as PluginEventEnvelope); + })); + + this.logger.info(pluginId, `Subscribed to server event ${eventName}`); + + return { + dispose: () => { + subscription.unsubscribe(); + this.logger.info(pluginId, `Unsubscribed from server event ${eventName}`); + } + }; + } + + private receivePluginUserMessage(pluginId: string, request: PluginApiMessageAsPluginUserRequest): void { + const roomId = this.requireRoomId(); + const message: Message = { + channelId: request.channelId ?? this.activeChannelId() ?? undefined, + content: request.content, + id: createId(), + isDeleted: false, + reactions: [], + roomId, + senderId: request.pluginUserId, + senderName: request.pluginUserId, + timestamp: Date.now() + }; + + this.logger.info(pluginId, 'Plugin user message emitted', { messageId: message.id }); + this.persistPluginMessage(pluginId, message); + this.store.dispatch(MessagesActions.receiveMessage({ message })); + this.voice.broadcastMessage({ type: 'chat-message', message } as unknown as ChatEvent); + } + + private deletePluginMessage(pluginId: string, messageId: string): void { + this.persistPluginMessageUpdate(pluginId, messageId, { + content: '[Message deleted]', + editedAt: Date.now(), + isDeleted: true + }); + + this.store.dispatch(MessagesActions.deleteMessageSuccess({ messageId })); + this.voice.broadcastMessage({ + deletedAt: Date.now(), + messageId, + type: 'message-deleted' + } as unknown as ChatEvent); + } + + private editPluginMessage(pluginId: string, messageId: string, content: string): void { + const editedAt = Date.now(); + + this.persistPluginMessageUpdate(pluginId, messageId, { content, editedAt }); + + this.store.dispatch(MessagesActions.editMessageSuccess({ + content, + editedAt, + messageId + })); + + this.voice.broadcastMessage({ + content, + editedAt, + messageId, + type: 'message-edited' + } as unknown as ChatEvent); + } + + private sendPluginMessage(pluginId: string, content: string, channelId?: string): Message { + const currentUser = this.currentUser(); + const roomId = this.requireRoomId(); + const message: Message = { + channelId: channelId ?? this.activeChannelId() ?? 'general', + content, + id: createId(), + isDeleted: false, + reactions: [], + roomId, + senderId: currentUser?.id ?? 'plugin', + senderName: currentUser?.displayName || currentUser?.username || 'Plugin', + timestamp: Date.now() + }; + + this.persistPluginMessage(pluginId, message); + this.store.dispatch(MessagesActions.sendMessageSuccess({ message })); + this.voice.broadcastMessage({ type: 'chat-message', message } as unknown as ChatEvent); + + return message; + } + + private setTyping(pluginId: string, isTyping: boolean, channelId?: string): void { + const roomId = this.requireRoomId(); + + try { + this.realtime.sendRawMessage({ + type: 'typing', + serverId: roomId, + channelId: channelId ?? this.activeChannelId() ?? 'general', + isTyping + }); + } catch (error: unknown) { + this.logger.warn(pluginId, 'Failed to publish typing state', error); + } + } + + private subscribeTyping(pluginId: string, handler: (event: PluginApiTypingEvent) => void): TojuPluginDisposable { + const subscription = new Subscription(); + + subscription.add(this.realtime.onSignalingMessage.subscribe((message) => { + const record = message as Record; + + if (record['type'] !== 'user_typing') { + return; + } + + const serverId = typeof record['serverId'] === 'string' ? record['serverId'] : ''; + const currentServer = this.currentRoom(); + + if (!serverId || serverId !== currentServer?.id) { + return; + } + + const userId = typeof record['oderId'] === 'string' ? record['oderId'] : ''; + const displayName = typeof record['displayName'] === 'string' ? record['displayName'] : userId; + const channelId = typeof record['channelId'] === 'string' && record['channelId'].trim() + ? record['channelId'].trim() + : 'general'; + const user = this.users().find((entry) => entry.oderId === userId || entry.id === userId) ?? null; + const channels = this.currentRoomChannels(); + + handler({ + channelId, + displayName, + isTyping: record['isTyping'] !== false, + server: currentServer, + serverId, + textChannel: channels.find((channel) => channel.type === 'text' && channel.id === channelId) ?? null, + user, + userId, + voiceChannel: user?.voiceState?.roomId + ? channels.find((channel) => channel.type === 'voice' && channel.id === user.voiceState?.roomId) ?? null + : null + }); + })); + + this.logger.info(pluginId, 'Subscribed to typing events'); + + return { + dispose: () => { + subscription.unsubscribe(); + this.logger.info(pluginId, 'Unsubscribed from typing events'); + } + }; + } + + private persistPluginMessage(pluginId: string, message: Message): void { + void this.db.saveMessage(message).catch((error: unknown) => { + this.logger.warn(pluginId, 'Failed to persist plugin message', error); + }); + } + + private persistPluginMessageUpdate(pluginId: string, messageId: string, updates: Partial): void { + void this.db.updateMessage(messageId, updates).catch((error: unknown) => { + this.logger.warn(pluginId, 'Failed to persist plugin message update', error); + }); + } + + private rememberSubscription(pluginId: string, eventName: string) { + this.logger.info(pluginId, `Subscribed to ${eventName}`); + + return { + dispose: () => this.logger.info(pluginId, `Unsubscribed from ${eventName}`) + }; + } + + private requireRoomId(): string { + const roomId = this.currentRoomId(); + + if (!roomId) { + throw new Error('No active server'); + } + + return roomId; + } + + private updateRoomAccessControl(changes: Parameters[0]['changes']): void { + this.store.dispatch(RoomsActions.updateRoomAccessControl({ + changes, + roomId: this.requireRoomId() + })); + } +} + +function createChannel(request: PluginApiChannelRequest, type: Channel['type']): Channel { + return { + id: request.id ?? slug(request.name), + name: request.name, + position: request.position ?? Date.now(), + type + }; +} + +function createId(): string { + return globalThis.crypto?.randomUUID?.() ?? `plugin-${Date.now()}-${Math.random().toString(36) + .slice(2)}`; +} + +function deepFreeze(value: TValue): TValue { + for (const propertyValue of Object.values(value)) { + if (propertyValue && typeof propertyValue === 'object') { + deepFreeze(propertyValue as Record); + } + } + + return Object.freeze(value); +} + +async function playAudioClip(url: string, volume = 1): Promise { + const audio = new Audio(url); + + audio.volume = Math.max(0, Math.min(1, volume)); + await audio.play(); +} + +function slug(value: string): string { + return value.trim().toLowerCase() + .replace(/[^a-z0-9.-]+/g, '-') + .replace(/(^-+|-+$)/g, '') || createId(); +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-desktop-state.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-desktop-state.service.ts new file mode 100644 index 0000000..99beec2 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-desktop-state.service.ts @@ -0,0 +1,56 @@ +import { Injectable, inject } from '@angular/core'; +import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage'; +import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service'; + +@Injectable({ providedIn: 'root' }) +export class PluginDesktopStateService { + private readonly electronBridge = inject(ElectronBridgeService); + + async readJson(key: string, fallback: TValue): Promise { + const raw = await this.readRaw(key); + + if (!raw) { + return fallback; + } + + try { + return JSON.parse(raw) as TValue; + } catch { + return fallback; + } + } + + async writeJson(key: string, value: unknown): Promise { + await this.writeRaw(key, JSON.stringify(value)); + } + + private async readRaw(key: string): Promise { + const scopedKey = getUserScopedStorageKey(key); + const api = this.electronBridge.getApi(); + + if (api) { + return await api.query({ + type: 'get-meta', + payload: { key: scopedKey } + }); + } + + return localStorage.getItem(scopedKey); + } + + private async writeRaw(key: string, value: string): Promise { + const scopedKey = getUserScopedStorageKey(key); + const api = this.electronBridge.getApi(); + + if (api) { + await api.command({ + type: 'save-meta', + payload: { key: scopedKey, value } + }); + + return; + } + + localStorage.setItem(scopedKey, value); + } +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-host.service.spec.ts b/toju-app/src/app/domains/plugins/application/services/plugin-host.service.spec.ts new file mode 100644 index 0000000..e381a93 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-host.service.spec.ts @@ -0,0 +1,169 @@ +import { Injector } from '@angular/core'; +import type { TojuPluginManifest } from '../../../../shared-kernel'; +import { DEVELOPMENT_PLUGIN_MANIFEST } from '../../development/development-plugin'; +import type { LocalPluginDiscoveryResult } from '../../domain/models/plugin-runtime.models'; +import { LocalPluginDiscoveryService } from '../../infrastructure/local-plugin-discovery.service'; +import { PluginCapabilityService } from './plugin-capability.service'; +import { PluginClientApiService } from './plugin-client-api.service'; +import { PluginDesktopStateService } from './plugin-desktop-state.service'; +import { PluginHostService } from './plugin-host.service'; +import { PluginLoggerService } from './plugin-logger.service'; +import { PluginRegistryService } from './plugin-registry.service'; +import { PluginUiRegistryService } from './plugin-ui-registry.service'; + +const TEST_PLUGIN_MANIFEST = createTestPluginManifest(); + +describe('PluginHostService', () => { + let discoveryResult: LocalPluginDiscoveryResult; + + beforeEach(() => { + discoveryResult = { + errors: [], + plugins: [], + pluginsPath: '/plugins' + }; + }); + + it('registers discovered test plugin manifests', async () => { + discoveryResult = { + errors: [], + plugins: [ + { + discoveredAt: 1, + entrypointPath: '/plugins/api-test-plugin/dist/main.js', + manifest: TEST_PLUGIN_MANIFEST, + manifestPath: '/plugins/api-test-plugin/toju-plugin.json', + pluginRoot: '/plugins/api-test-plugin', + readmePath: '/plugins/api-test-plugin/README.md' + } + ], + pluginsPath: '/plugins' + }; + + const host = createHostService(() => discoveryResult); + const result = await host.discoverLocalPlugins(); + + expect(result.errors).toEqual([]); + expect(result.registered.map((plugin) => plugin.manifest.id)).toEqual([TEST_PLUGIN_MANIFEST.id]); + const readyManifestIds = host.getReadyManifests().map((manifest) => manifest.id); + + expect(readyManifestIds.sort()).toEqual([DEVELOPMENT_PLUGIN_MANIFEST.id, TEST_PLUGIN_MANIFEST.id].sort()); + }); + + it('registers the built-in development plugin in development builds', () => { + const host = createHostService(() => discoveryResult); + + expect(host.getReadyManifests().map((manifest) => manifest.id)).toEqual([DEVELOPMENT_PLUGIN_MANIFEST.id]); + }); + + it('keeps discovery and validation failures visible to callers', async () => { + discoveryResult = { + errors: [ + { + manifestPath: '/plugins/broken/plugin.json', + message: 'Unexpected end of JSON input', + pluginRoot: '/plugins/broken' + } + ], + plugins: [ + { + discoveredAt: 1, + manifest: { + ...TEST_PLUGIN_MANIFEST, + entrypoint: undefined + }, + manifestPath: '/plugins/invalid/toju-plugin.json', + pluginRoot: '/plugins/invalid' + } + ], + pluginsPath: '/plugins' + }; + + const host = createHostService(() => discoveryResult); + const result = await host.discoverLocalPlugins(); + + expect(result.registered).toEqual([]); + expect(result.errors.map((error) => error.pluginRoot)).toEqual(['/plugins/broken', '/plugins/invalid']); + + expect(result.errors[1]?.message).toContain('client plugins require an entrypoint'); + }); +}); + +function createHostService(readDiscoveryResult: () => LocalPluginDiscoveryResult): PluginHostService { + const injector = Injector.create({ + providers: [ + PluginHostService, + PluginRegistryService, + { + provide: PluginCapabilityService, + useValue: { + missing: vi.fn(() => []) + } + }, + { + provide: PluginDesktopStateService, + useValue: { + readJson: vi.fn(async (_key: string, fallback: unknown) => fallback), + writeJson: vi.fn(async () => undefined) + } + }, + { + provide: PluginClientApiService, + useValue: { + createApi: vi.fn(() => ({})) + } + }, + { + provide: PluginLoggerService, + useValue: { + error: vi.fn(), + info: vi.fn(), + warn: vi.fn() + } + }, + { + provide: PluginUiRegistryService, + useValue: { + unregisterPlugin: vi.fn() + } + }, + { + provide: LocalPluginDiscoveryService, + useValue: { + discoverManifests: vi.fn(async () => readDiscoveryResult()) + } + } + ] + }); + + return injector.get(PluginHostService); +} + +function createTestPluginManifest(): TojuPluginManifest { + return { + apiVersion: '1.0.0', + capabilities: [ + 'storage.serverData.read', + 'storage.serverData.write', + 'events.server.publish' + ], + compatibility: { + minimumTojuVersion: '1.0.0' + }, + description: 'Fixture plugin used by automated tests for plugin support APIs.', + entrypoint: './dist/main.js', + events: [ + { + direction: 'serverRelay', + eventName: 'e2e:relay', + maxPayloadBytes: 2048, + scope: 'server' + } + ], + id: 'e2e.plugin-api', + kind: 'client', + schemaVersion: 1, + title: 'E2E Plugin API Fixture', + version: '1.0.0' + }; +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-host.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-host.service.ts new file mode 100644 index 0000000..729cee9 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-host.service.ts @@ -0,0 +1,533 @@ +import { Injectable, inject } from '@angular/core'; +import { environment } from '../../../../../environments/environment'; +import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service'; +import type { TojuPluginManifest } from '../../../../shared-kernel'; +import { + DEVELOPMENT_PLUGIN_ENTRYPOINT, + DEVELOPMENT_PLUGIN_MANIFEST, + DEVELOPMENT_PLUGIN_MODULE +} from '../../development/development-plugin'; +import type { + TojuClientPluginModule, + TojuPluginActivationContext, + TojuPluginDisposable +} from '../../domain/models/plugin-api.models'; +import type { + LocalPluginDiscoveryError, + LocalPluginRegistrationResult, + RegisteredPlugin +} from '../../domain/models/plugin-runtime.models'; +import { LocalPluginDiscoveryService } from '../../infrastructure/local-plugin-discovery.service'; +import { PluginCapabilityService } from './plugin-capability.service'; +import { PluginDesktopStateService } from './plugin-desktop-state.service'; +import { PluginClientApiService } from './plugin-client-api.service'; +import { PluginLoggerService } from './plugin-logger.service'; +import { PluginRegistryService } from './plugin-registry.service'; +import { PluginUiRegistryService } from './plugin-ui-registry.service'; + +interface ActivePluginRuntime { + context: TojuPluginActivationContext; + moduleObjectUrl?: string; + module: TojuClientPluginModule; +} + +const STORAGE_KEY_PLUGIN_ACTIVATION = 'metoyou_plugin_activation_state'; + +@Injectable({ providedIn: 'root' }) +export class PluginHostService { + private readonly apiFactory = inject(PluginClientApiService); + private readonly capabilities = inject(PluginCapabilityService); + private readonly desktopState = inject(PluginDesktopStateService); + private readonly electronBridge = inject(ElectronBridgeService, { optional: true }); + private readonly localDiscovery = inject(LocalPluginDiscoveryService); + private readonly logger = inject(PluginLoggerService); + private readonly registry = inject(PluginRegistryService); + private readonly uiRegistry = inject(PluginUiRegistryService); + private readonly activePlugins = new Map(); + private readonly activationRequests = new Map>(); + private readonly activationStateReady: Promise; + private activatedPluginIds = new Set(); + + constructor() { + this.registerDevelopmentPlugin(); + this.activationStateReady = this.loadActivationState(); + } + + registerLocalManifest(manifestValue: unknown, sourcePath?: string): RegisteredPlugin { + return this.registry.registerManifest(manifestValue, sourcePath); + } + + async discoverLocalPlugins(): Promise { + const discovery = await this.localDiscovery.discoverManifests(); + const registered: RegisteredPlugin[] = []; + const errors: LocalPluginDiscoveryError[] = [...discovery.errors]; + + for (const descriptor of discovery.plugins) { + try { + registered.push(this.registerLocalManifest(descriptor.manifest, descriptor.pluginRootUrl ?? descriptor.pluginRoot)); + } catch (error) { + errors.push({ + manifestPath: descriptor.manifestPath, + message: error instanceof Error ? error.message : 'Plugin manifest validation failed', + pluginRoot: descriptor.pluginRoot + }); + } + } + + return { + discovery, + errors, + registered + }; + } + + getReadyManifests(): TojuPluginManifest[] { + return this.registry.loadOrder().ordered; + } + + async activateReadyPlugins(): Promise { + await this.activationStateReady; + + const activated: TojuPluginActivationContext[] = []; + + for (const manifest of this.registry.loadOrder().ordered) { + const entry = this.registry.find(manifest.id); + + if (!entry || !entry.enabled || this.activePlugins.has(manifest.id)) { + continue; + } + + const didActivate = await this.activatePlugin(entry); + const active = this.activePlugins.get(manifest.id); + + if (didActivate && active) { + activated.push(active.context); + this.activatedPluginIds.add(active.context.pluginId); + } + } + + await this.saveActivationState(); + + await this.runReadyHooks(activated); + } + + async activatePluginById(pluginId: string): Promise { + await this.activationStateReady; + + if (this.activePlugins.has(pluginId)) { + this.activatedPluginIds.add(pluginId); + await this.saveActivationState(); + return; + } + + const entry = this.registry.find(pluginId); + + if (!entry?.enabled) { + return; + } + + const didActivate = await this.activatePlugin(entry); + const active = this.activePlugins.get(pluginId); + + if (!didActivate || !active) { + return; + } + + this.activatedPluginIds.add(pluginId); + await this.saveActivationState(); + await this.runReadyHooks([active.context]); + } + + async rememberActivation(pluginId: string): Promise { + await this.activationStateReady; + this.activatedPluginIds.add(pluginId); + await this.saveActivationState(); + } + + async activatePersistedPlugins(): Promise { + await this.activationStateReady; + + const activated: TojuPluginActivationContext[] = []; + + for (const manifest of this.registry.loadOrder().ordered) { + if (!this.activatedPluginIds.has(manifest.id) || this.activePlugins.has(manifest.id)) { + continue; + } + + const entry = this.registry.find(manifest.id); + + if (!entry?.enabled) { + continue; + } + + const didActivate = await this.activatePlugin(entry); + const active = this.activePlugins.get(manifest.id); + + if (didActivate && active) { + activated.push(active.context); + } + } + + await this.runReadyHooks(activated); + } + + isPluginActive(pluginId: string): boolean { + return this.activePlugins.has(pluginId); + } + + async deactivatePlugin(pluginId: string, options: { forgetActivation?: boolean } = {}): Promise { + await this.activationStateReady; + + const active = this.activePlugins.get(pluginId); + + if (!active) { + if (options.forgetActivation) { + this.activatedPluginIds.delete(pluginId); + await this.saveActivationState(); + } + + this.registry.setState(pluginId, 'unloaded'); + this.uiRegistry.unregisterPlugin(pluginId); + return; + } + + this.registry.setState(pluginId, 'unloading'); + + try { + await active.module.deactivate?.(active.context); + } catch (error) { + this.logger.warn(pluginId, 'Plugin deactivate failed', error); + } + + for (const disposable of [...active.context.subscriptions].reverse()) { + safeDispose(disposable, pluginId, this.logger); + } + + this.uiRegistry.unregisterPlugin(pluginId); + this.activePlugins.delete(pluginId); + this.revokeModuleObjectUrl(pluginId); + + if (options.forgetActivation) { + this.activatedPluginIds.delete(pluginId); + await this.saveActivationState(); + } + + this.registry.setState(pluginId, 'unloaded'); + } + + async deactivateAll(): Promise { + const pluginIds = Array.from(this.activePlugins.keys()).reverse(); + + for (const pluginId of pluginIds) { + await this.deactivatePlugin(pluginId); + } + } + + async reloadPlugin(pluginId: string): Promise { + await this.deactivatePlugin(pluginId); + + const entry = this.registry.find(pluginId); + + if (entry?.enabled) { + await this.activatePlugin(entry); + + if (this.activePlugins.has(pluginId)) { + this.activatedPluginIds.add(pluginId); + await this.saveActivationState(); + } + } + } + + markLoaded(pluginId: string): void { + this.registry.setState(pluginId, 'loaded'); + } + + markFailed(pluginId: string): void { + this.registry.setState(pluginId, 'failed'); + } + + private async runReadyHooks(contexts: TojuPluginActivationContext[]): Promise { + for (const context of contexts) { + const active = this.activePlugins.get(context.pluginId); + + if (!active?.module.ready) { + continue; + } + + try { + await active.module.ready(context); + this.registry.setState(context.pluginId, 'ready'); + } catch (error) { + this.failPlugin(context.pluginId, error); + } + } + } + + private async activatePlugin(entry: RegisteredPlugin): Promise { + const pluginId = entry.manifest.id; + + if (this.activePlugins.has(pluginId)) { + return false; + } + + const pendingActivation = this.activationRequests.get(pluginId); + + if (pendingActivation) { + await pendingActivation; + return false; + } + + const activation = this.activatePluginInternal(entry); + + this.activationRequests.set(pluginId, activation); + + try { + return await activation; + } finally { + if (this.activationRequests.get(pluginId) === activation) { + this.activationRequests.delete(pluginId); + } + } + } + + private async activatePluginInternal(entry: RegisteredPlugin): Promise { + const manifest = entry.manifest; + const missingCapabilities = this.capabilities.missing(manifest); + + if (missingCapabilities.length > 0) { + this.registry.setFailed(manifest.id, `Missing capabilities: ${missingCapabilities.join(', ')}`); + this.logger.warn(manifest.id, 'Plugin blocked by missing capability grants', missingCapabilities); + return false; + } + + if (!manifest.entrypoint) { + this.registry.setState(manifest.id, 'ready'); + return false; + } + + this.registry.setState(manifest.id, 'loading'); + + try { + const { module, moduleObjectUrl } = await this.loadPluginModule(manifest, entry.sourcePath); + const context: TojuPluginActivationContext = { + api: this.apiFactory.createApi(manifest), + manifest, + pluginId: manifest.id, + subscriptions: [] + }; + + await this.runWithPluginRuntimeGuards(manifest.id, () => module.activate?.(context)); + this.activePlugins.set(manifest.id, { context, module, moduleObjectUrl }); + this.registry.setState(manifest.id, 'loaded'); + this.logger.info(manifest.id, 'Plugin activated'); + return true; + } catch (error) { + this.failPlugin(manifest.id, error); + return false; + } + } + + private failPlugin(pluginId: string, error: unknown): void { + const message = error instanceof Error ? error.message : 'Plugin activation failed'; + + this.registry.setFailed(pluginId, message); + this.logger.error(pluginId, message, error); + this.uiRegistry.unregisterPlugin(pluginId); + this.activePlugins.delete(pluginId); + this.revokeModuleObjectUrl(pluginId); + } + + private async runWithPluginRuntimeGuards(pluginId: string, activate: () => Promise | void): Promise { + const originalMutationObserver = globalThis.MutationObserver; + + if (!originalMutationObserver) { + await activate(); + return; + } + + const guardedMutationObserver = createGuardedMutationObserver(originalMutationObserver, pluginId, this.logger); + + globalThis.MutationObserver = guardedMutationObserver; + + try { + await activate(); + } finally { + if (globalThis.MutationObserver === guardedMutationObserver) { + globalThis.MutationObserver = originalMutationObserver; + } + } + } + + private async loadPluginModule( + manifest: TojuPluginManifest, + sourcePath?: string + ): Promise<{ module: TojuClientPluginModule; moduleObjectUrl?: string }> { + if (manifest.entrypoint === DEVELOPMENT_PLUGIN_ENTRYPOINT) { + return { module: DEVELOPMENT_PLUGIN_MODULE }; + } + + const entrypointUrl = this.resolveEntrypoint(manifest, sourcePath); + + if (entrypointUrl.startsWith('file://')) { + const moduleObjectUrl = await this.createLocalModuleObjectUrl(entrypointUrl); + const module = await import(/* @vite-ignore */ moduleObjectUrl) as TojuClientPluginModule; + + return { module, moduleObjectUrl }; + } + + return { + module: await import(/* @vite-ignore */ entrypointUrl) as TojuClientPluginModule + }; + } + + private async createLocalModuleObjectUrl(entrypointUrl: string): Promise { + const api = this.electronBridge?.getApi(); + + if (!api) { + throw new Error('Local plugin entrypoints require the desktop app'); + } + + const base64Data = await api.readFile(fileUrlToPath(entrypointUrl)); + const bytes = Uint8Array.from(atob(base64Data), (character) => character.charCodeAt(0)); + const source = new TextDecoder().decode(bytes); + + return URL.createObjectURL(new Blob([source], { type: 'text/javascript' })); + } + + private revokeModuleObjectUrl(pluginId: string): void { + const moduleObjectUrl = this.activePlugins.get(pluginId)?.moduleObjectUrl; + + if (moduleObjectUrl) { + URL.revokeObjectURL(moduleObjectUrl); + } + } + + private registerDevelopmentPlugin(): void { + if (environment.production) { + return; + } + + try { + this.registry.registerManifest(DEVELOPMENT_PLUGIN_MANIFEST, DEVELOPMENT_PLUGIN_ENTRYPOINT); + } catch (error) { + this.logger.warn(DEVELOPMENT_PLUGIN_MANIFEST.id, 'Development plugin registration failed', error); + } + } + + private async loadActivationState(): Promise { + const state = await this.desktopState.readJson<{ activatedPluginIds?: string[] }>(STORAGE_KEY_PLUGIN_ACTIVATION, {}); + + this.activatedPluginIds = new Set( + Array.isArray(state.activatedPluginIds) + ? state.activatedPluginIds.filter((pluginId): pluginId is string => typeof pluginId === 'string') + : [] + ); + } + + private async saveActivationState(): Promise { + await this.desktopState.writeJson(STORAGE_KEY_PLUGIN_ACTIVATION, { + activatedPluginIds: Array.from(this.activatedPluginIds).sort() + }); + } + + private resolveEntrypoint(manifest: TojuPluginManifest, sourcePath?: string): string { + if (!manifest.entrypoint) { + throw new Error('Plugin entrypoint is missing'); + } + + try { + return new URL(manifest.entrypoint).toString(); + } catch {} + + if (manifest.bundle?.url && !sourcePath?.startsWith('file://')) { + return manifest.bundle.url; + } + + if (sourcePath?.startsWith('http://') || sourcePath?.startsWith('https://') || sourcePath?.startsWith('file://')) { + return new URL(manifest.entrypoint, sourcePath).toString(); + } + + if (manifest.entrypoint.startsWith('/')) { + return manifest.entrypoint; + } + + throw new Error(`Plugin ${manifest.id} has no browser-importable entrypoint`); + } +} + +function fileUrlToPath(fileUrl: string): string { + const url = new URL(fileUrl); + const decodedPath = decodeURIComponent(url.pathname); + + if (/^\/[A-Za-z]:\//.test(decodedPath)) { + return decodedPath.slice(1).replace(/\//g, '\\'); + } + + return decodedPath; +} + +function safeDispose(disposable: TojuPluginDisposable, pluginId: string, logger: PluginLoggerService): void { + try { + disposable.dispose(); + } catch (error) { + logger.warn(pluginId, 'Plugin disposable failed', error); + } +} + +function createGuardedMutationObserver( + NativeMutationObserver: typeof MutationObserver, + pluginId: string, + logger: PluginLoggerService +): typeof MutationObserver { + return class GuardedPluginMutationObserver implements MutationObserver { + private readonly nativeObserver: MutationObserver; + private readonly observations: { options?: MutationObserverInit; target: Node }[] = []; + private isDispatching = false; + + constructor(private readonly callback: MutationCallback) { + this.nativeObserver = new NativeMutationObserver((records) => this.dispatch(records)); + } + + observe(target: Node, options?: MutationObserverInit): void { + const existing = this.observations.find((observation) => observation.target === target); + + if (existing) { + existing.options = options; + } else { + this.observations.push({ options, target }); + } + + this.nativeObserver.observe(target, options); + } + + disconnect(): void { + this.observations.length = 0; + this.nativeObserver.disconnect(); + } + + takeRecords(): MutationRecord[] { + return this.nativeObserver.takeRecords(); + } + + private dispatch(records: MutationRecord[]): void { + if (this.isDispatching) { + return; + } + + this.isDispatching = true; + this.nativeObserver.disconnect(); + + try { + this.callback(records, this); + } catch (error) { + logger.warn(pluginId, 'Plugin MutationObserver callback failed', error); + } finally { + this.isDispatching = false; + + for (const observation of this.observations) { + this.nativeObserver.observe(observation.target, observation.options); + } + } + } + }; +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-logger.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-logger.service.ts new file mode 100644 index 0000000..3d62bbf --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-logger.service.ts @@ -0,0 +1,64 @@ +import { + Injectable, + Signal, + computed, + signal +} from '@angular/core'; + +export type PluginLogLevel = 'debug' | 'error' | 'info' | 'warn'; + +export interface PluginLogEntry { + data?: unknown; + level: PluginLogLevel; + message: string; + pluginId: string; + timestamp: number; +} + +@Injectable({ providedIn: 'root' }) +export class PluginLoggerService { + readonly entries: Signal; + + private readonly entriesSignal = signal([]); + + constructor() { + this.entries = this.entriesSignal.asReadonly(); + } + + entriesFor(pluginId: string): Signal { + return computed(() => this.entries().filter((entry) => entry.pluginId === pluginId)); + } + + debug(pluginId: string, message: string, data?: unknown): void { + this.add(pluginId, 'debug', message, data); + } + + error(pluginId: string, message: string, data?: unknown): void { + this.add(pluginId, 'error', message, data); + } + + info(pluginId: string, message: string, data?: unknown): void { + this.add(pluginId, 'info', message, data); + } + + warn(pluginId: string, message: string, data?: unknown): void { + this.add(pluginId, 'warn', message, data); + } + + clear(pluginId?: string): void { + this.entriesSignal.update((entries) => pluginId ? entries.filter((entry) => entry.pluginId !== pluginId) : []); + } + + private add(pluginId: string, level: PluginLogLevel, message: string, data?: unknown): void { + this.entriesSignal.update((entries) => [ + ...entries, + { + data, + level, + message, + pluginId, + timestamp: Date.now() + } + ].slice(-500)); + } +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-message-bus.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-message-bus.service.ts new file mode 100644 index 0000000..77ec922 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-message-bus.service.ts @@ -0,0 +1,236 @@ +import { Injectable, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Subscription } from 'rxjs'; +import { RealtimeSessionFacade } from '../../../../core/realtime'; +import type { + ChatEvent, + Message, + User +} from '../../../../shared-kernel'; +import { selectCurrentRoomMessages } from '../../../../store/messages/messages.selectors'; +import { selectCurrentRoomId } from '../../../../store/rooms/rooms.selectors'; +import { selectCurrentUser } from '../../../../store/users/users.selectors'; +import type { + PluginApiMessageBusEnvelope, + PluginApiMessageBusLatestRequest, + PluginApiMessageBusPublishRequest, + PluginApiMessageBusSubscription, + TojuPluginDisposable +} from '../../domain/models/plugin-api.models'; + +const DEFAULT_LATEST_MESSAGE_LIMIT = 50; +const MAX_LATEST_MESSAGE_LIMIT = 250; +const LATEST_MESSAGES_TOPIC = 'latest-messages'; + +@Injectable({ providedIn: 'root' }) +export class PluginMessageBusService { + private readonly realtime = inject(RealtimeSessionFacade); + private readonly store = inject(Store); + private readonly currentMessages = this.store.selectSignal(selectCurrentRoomMessages); + private readonly currentRoomId = this.store.selectSignal(selectCurrentRoomId); + private readonly currentUser = this.store.selectSignal(selectCurrentUser); + private readonly localSubscriptions = new Map>(); + + publish(pluginId: string, request: PluginApiMessageBusPublishRequest): PluginApiMessageBusEnvelope { + const envelope = this.createEnvelope(pluginId, request.topic, request.payload, request, request.includeLatestMessages === true); + + this.sendEnvelope(envelope, request.targetPeerId); + + if (request.includeSelf) { + this.dispatchLocal(envelope); + } + + return envelope; + } + + sendLatestMessages(pluginId: string, request: PluginApiMessageBusLatestRequest = {}): PluginApiMessageBusEnvelope { + const envelope = this.createEnvelope(pluginId, request.topic ?? LATEST_MESSAGES_TOPIC, undefined, request, true); + + this.sendEnvelope(envelope, request.targetPeerId); + + return envelope; + } + + subscribe(pluginId: string, subscription: PluginApiMessageBusSubscription): TojuPluginDisposable { + const pluginSubscriptions = this.localSubscriptions.get(pluginId) ?? new Set(); + const realtimeSubscription = new Subscription(); + + pluginSubscriptions.add(subscription); + this.localSubscriptions.set(pluginId, pluginSubscriptions); + + realtimeSubscription.add(this.realtime.onMessageReceived.subscribe((event) => { + const envelope = readPluginMessageBusEnvelope(event); + + if (!envelope || envelope.pluginId !== pluginId || !matchesSubscription(envelope, subscription)) { + return; + } + + subscription.handler(envelope); + })); + + if (subscription.replayLatest) { + subscription.handler(this.createEnvelope( + pluginId, + subscription.topic ?? LATEST_MESSAGES_TOPIC, + undefined, + { + channelId: subscription.channelId, + limit: subscription.latestMessageLimit + }, + true + )); + } + + return { + dispose: () => { + realtimeSubscription.unsubscribe(); + pluginSubscriptions.delete(subscription); + + if (pluginSubscriptions.size === 0) { + this.localSubscriptions.delete(pluginId); + } + } + }; + } + + private createEnvelope( + pluginId: string, + topic: string, + payload: unknown, + request: PluginApiMessageBusLatestRequest, + includeMessages: boolean + ): PluginApiMessageBusEnvelope { + const currentUser = this.currentUser() ?? null; + const envelope: PluginApiMessageBusEnvelope = { + eventId: createId(), + pluginId, + roomId: this.requireRoomId(), + sentAt: Date.now(), + sourceUserId: readUserId(currentUser), + topic + }; + + if (request.channelId) { + envelope.channelId = request.channelId; + } + + if (payload !== undefined) { + envelope.payload = payload; + } + + if (includeMessages) { + envelope.messages = this.latestMessages(request); + } + + return envelope; + } + + private latestMessages(request: PluginApiMessageBusLatestRequest): Message[] { + const limit = clampLimit(request.limit); + const sinceTimestamp = typeof request.sinceTimestamp === 'number' ? request.sinceTimestamp : null; + + return this.currentMessages() + .filter((message) => !request.channelId || message.channelId === request.channelId) + .filter((message) => request.includeDeleted || !message.isDeleted) + .filter((message) => sinceTimestamp === null || message.timestamp > sinceTimestamp) + .slice(-limit); + } + + private sendEnvelope(envelope: PluginApiMessageBusEnvelope, targetPeerId?: string): void { + const event: ChatEvent = { + pluginMessage: envelope, + roomId: envelope.roomId, + timestamp: envelope.sentAt, + type: 'plugin-message-bus' + }; + + if (targetPeerId) { + this.realtime.sendToPeer(targetPeerId, event); + return; + } + + this.realtime.broadcastMessage(event); + } + + private dispatchLocal(envelope: PluginApiMessageBusEnvelope): void { + for (const subscription of this.localSubscriptions.get(envelope.pluginId) ?? []) { + if (matchesSubscription(envelope, subscription)) { + subscription.handler(envelope); + } + } + } + + private requireRoomId(): string { + const roomId = this.currentRoomId(); + + if (!roomId) { + throw new Error('No active server'); + } + + return roomId; + } +} + +function readPluginMessageBusEnvelope(event: ChatEvent): PluginApiMessageBusEnvelope | null { + if (event.type !== 'plugin-message-bus' || !isRecord(event.pluginMessage)) { + return null; + } + + const envelope = event.pluginMessage; + + if (typeof envelope['eventId'] !== 'string' + || typeof envelope['pluginId'] !== 'string' + || typeof envelope['roomId'] !== 'string' + || typeof envelope['sentAt'] !== 'number' + || typeof envelope['topic'] !== 'string') { + return null; + } + + return { + channelId: typeof envelope['channelId'] === 'string' ? envelope['channelId'] : undefined, + eventId: envelope['eventId'], + messages: Array.isArray(envelope['messages']) ? envelope['messages'].filter(isMessage) : undefined, + payload: envelope['payload'], + pluginId: envelope['pluginId'], + roomId: envelope['roomId'], + sentAt: envelope['sentAt'], + sourcePeerId: event.fromPeerId, + sourceUserId: typeof envelope['sourceUserId'] === 'string' ? envelope['sourceUserId'] : undefined, + topic: envelope['topic'] + }; +} + +function matchesSubscription(envelope: PluginApiMessageBusEnvelope, subscription: PluginApiMessageBusSubscription): boolean { + return (!subscription.topic || subscription.topic === envelope.topic) + && (!subscription.channelId || subscription.channelId === envelope.channelId); +} + +function clampLimit(limit: number | undefined): number { + if (typeof limit !== 'number' || !Number.isFinite(limit)) { + return DEFAULT_LATEST_MESSAGE_LIMIT; + } + + return Math.max(1, Math.min(MAX_LATEST_MESSAGE_LIMIT, Math.floor(limit))); +} + +function readUserId(user: User | null): string | undefined { + return user?.oderId || user?.id || undefined; +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object'; +} + +function isMessage(value: unknown): value is Message { + return isRecord(value) + && typeof value['id'] === 'string' + && typeof value['roomId'] === 'string' + && typeof value['senderId'] === 'string' + && typeof value['content'] === 'string' + && typeof value['timestamp'] === 'number'; +} + +function createId(): string { + return globalThis.crypto?.randomUUID?.() ?? `plugin-bus-${Date.now()}-${Math.random().toString(36) + .slice(2)}`; +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-registry.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-registry.service.ts new file mode 100644 index 0000000..bdb7c8a --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-registry.service.ts @@ -0,0 +1,155 @@ +import { + Injectable, + type Signal, + computed, + inject, + signal +} from '@angular/core'; +import { + RegisteredPlugin, + type PluginLoadOrderResult, + type PluginRuntimeState +} from '../../domain/models/plugin-runtime.models'; +import { resolvePluginLoadOrder } from '../../domain/logic/plugin-dependency-resolver.logic'; +import { validateTojuPluginManifest } from '../../domain/logic/plugin-manifest-validation.logic'; +import { PluginDesktopStateService } from './plugin-desktop-state.service'; + +const STORAGE_KEY_PLUGIN_REGISTRY_STATE = 'metoyou_plugin_registry_state'; + +@Injectable({ providedIn: 'root' }) +export class PluginRegistryService { + readonly entries: Signal; + readonly enabledEntries: Signal; + readonly loadOrder: Signal; + + private readonly desktopState = inject(PluginDesktopStateService); + private readonly entriesSignal = signal([]); + private disabledPluginIds = new Set(); + + constructor() { + this.entries = this.entriesSignal.asReadonly(); + this.enabledEntries = computed(() => this.entries().filter((entry) => entry.enabled)); + this.loadOrder = computed(() => + resolvePluginLoadOrder(this.entries().map((entry) => ({ enabled: entry.enabled, manifest: entry.manifest }))) + ); + + void this.loadRegistryState(); + } + + clear(): void { + this.entriesSignal.set([]); + } + + registerManifest(manifestValue: unknown, sourcePath?: string): RegisteredPlugin { + const validation = validateTojuPluginManifest(manifestValue); + + if (!validation.manifest) { + throw new Error(validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join('\n')); + } + + const existingIndex = this.entries().findIndex((entry) => entry.manifest.id === validation.manifest?.id); + const entry: RegisteredPlugin = { + enabled: !this.disabledPluginIds.has(validation.manifest.id), + manifest: validation.manifest, + sourcePath, + state: validation.valid ? 'validated' : 'blocked', + validationIssues: validation.issues + }; + + if (existingIndex >= 0) { + this.entriesSignal.update((entries) => entries.map((candidate, index) => index === existingIndex ? entry : candidate)); + } else { + this.entriesSignal.update((entries) => [...entries, entry]); + } + + this.syncLoadState(); + return entry; + } + + setEnabled(pluginId: string, enabled: boolean): void { + if (enabled) { + this.disabledPluginIds.delete(pluginId); + } else { + this.disabledPluginIds.add(pluginId); + } + + this.entriesSignal.update((entries) => entries.map((entry) => entry.manifest.id === pluginId + ? { + ...entry, + enabled, + state: enabled ? entry.state === 'disabled' ? 'validated' : entry.state : 'disabled' + } + : entry)); + + this.syncLoadState(); + void this.saveRegistryState(); + } + + unregister(pluginId: string): void { + this.entriesSignal.update((entries) => entries.filter((entry) => entry.manifest.id !== pluginId)); + this.syncLoadState(); + } + + setState(pluginId: string, state: PluginRuntimeState): void { + this.entriesSignal.update((entries) => entries.map((entry) => entry.manifest.id === pluginId + ? { ...entry, error: undefined, state } + : entry)); + } + + setFailed(pluginId: string, error: string): void { + this.entriesSignal.update((entries) => entries.map((entry) => entry.manifest.id === pluginId + ? { ...entry, error, state: 'failed' } + : entry)); + } + + find(pluginId: string): RegisteredPlugin | undefined { + return this.entries().find((entry) => entry.manifest.id === pluginId); + } + + private syncLoadState(): void { + const loadOrder = this.loadOrder(); + const blockedIds = new Set(loadOrder.blocked.map((blocker) => blocker.pluginId)); + const loadIndexes = new Map(loadOrder.ordered.map((manifest, index) => [manifest.id, index])); + + this.entriesSignal.update((entries) => entries.map((entry) => { + const loadIndex = loadIndexes.get(entry.manifest.id); + + if (!entry.enabled) { + return { ...entry, loadIndex: undefined, state: 'disabled' }; + } + + if (blockedIds.has(entry.manifest.id)) { + return { ...entry, loadIndex: undefined, state: 'blocked' }; + } + + return { + ...entry, + loadIndex, + state: loadIndex === undefined ? entry.state : 'ready' + }; + })); + } + + private async loadRegistryState(): Promise { + const state = await this.desktopState.readJson<{ disabledPluginIds?: string[] }>(STORAGE_KEY_PLUGIN_REGISTRY_STATE, {}); + + this.disabledPluginIds = new Set( + Array.isArray(state.disabledPluginIds) + ? state.disabledPluginIds.filter((pluginId): pluginId is string => typeof pluginId === 'string') + : [] + ); + + this.entriesSignal.update((entries) => entries.map((entry) => ({ + ...entry, + enabled: !this.disabledPluginIds.has(entry.manifest.id) + }))); + + this.syncLoadState(); + } + + private async saveRegistryState(): Promise { + await this.desktopState.writeJson(STORAGE_KEY_PLUGIN_REGISTRY_STATE, { + disabledPluginIds: Array.from(this.disabledPluginIds).sort() + }); + } +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-requirement-state.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-requirement-state.service.ts new file mode 100644 index 0000000..ab1dd44 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-requirement-state.service.ts @@ -0,0 +1,339 @@ +/* eslint-disable @typescript-eslint/member-ordering */ +import { + DestroyRef, + Injectable, + computed, + effect, + inject, + signal +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Store } from '@ngrx/store'; +import type { + PluginRequirementSummary, + PluginRequirementsSnapshot, + TojuPluginManifest +} from '../../../../shared-kernel'; +import { RealtimeSessionFacade } from '../../../../core/realtime'; +import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage'; +import { selectCurrentRoom, selectCurrentRoomId } from '../../../../store/rooms/rooms.selectors'; +import { ServerDirectoryFacade, type ServerSourceSelector } from '../../../server-directory'; +import { PluginRegistryService } from './plugin-registry.service'; +import { PluginRequirementService } from './plugin-requirement.service'; +import { PluginStoreService } from './plugin-store.service'; + +const STORAGE_KEY_OPTIONAL_REQUIREMENT_DISMISSALS = 'metoyou_optional_plugin_requirement_dismissals'; + +type RequirementDismissalState = Record>; + +export type PluginRequirementComparisonStatus = + | 'blockedByServer' + | 'disabled' + | 'enabled' + | 'incompatible' + | 'missing' + | 'notRequired'; + +export interface PluginRequirementComparison { + installed?: TojuPluginManifest; + pluginId: string; + requirement?: PluginRequirementSummary; + status: PluginRequirementComparisonStatus; +} + +@Injectable({ providedIn: 'root' }) +export class PluginRequirementStateService { + private readonly destroyRef = inject(DestroyRef); + private readonly pluginRequirements = inject(PluginRequirementService); + private readonly pluginStore = inject(PluginStoreService); + private readonly realtime = inject(RealtimeSessionFacade); + private readonly registry = inject(PluginRegistryService); + private readonly serverDirectory = inject(ServerDirectoryFacade); + private readonly store = inject(Store); + + private readonly currentRoom = this.store.selectSignal(selectCurrentRoom); + private readonly currentRoomId = this.store.selectSignal(selectCurrentRoomId); + private readonly snapshotsSignal = signal>({}); + private readonly refreshErrorsSignal = signal>({}); + private readonly sessionDismissedOptionalSignal = signal>({}); + private readonly hiddenOptionalSignal = signal(loadRequirementDismissals()); + + readonly currentSnapshot = computed(() => { + const roomId = this.currentRoomId(); + + return roomId ? this.snapshotsSignal()[roomId] ?? null : null; + }); + readonly refreshErrors = this.refreshErrorsSignal.asReadonly(); + readonly missingInstallableRequirements = computed(() => { + if (!this.pluginStore.serverInstalledPluginsReadyForCurrentRoom()) { + return []; + } + + const requirements: PluginRequirementSummary[] = []; + + for (const comparison of this.comparisons()) { + if (this.isMissingInstallableRequirement(comparison) && comparison.requirement) { + requirements.push(comparison.requirement); + } + } + + return requirements; + }); + readonly missingRequiredRequirements = computed(() => this.missingInstallableRequirements() + .filter((requirement) => requirement.status === 'required')); + readonly visibleOptionalRequirements = computed(() => this.missingInstallableRequirements() + .filter((requirement) => requirement.status === 'optional' || requirement.status === 'recommended') + .filter((requirement) => !this.isOptionalRequirementDismissed(requirement))); + readonly comparisons = computed(() => { + const snapshot = this.currentSnapshot(); + const installedEntries = this.registry.entries(); + const installedById = new Map(installedEntries.map((entry) => [entry.manifest.id, entry])); + const requirementIds = new Set(snapshot?.requirements.map((requirement) => requirement.pluginId) ?? []); + const comparisons: PluginRequirementComparison[] = []; + + for (const requirement of snapshot?.requirements ?? []) { + const entry = installedById.get(requirement.pluginId); + + comparisons.push({ + installed: entry?.manifest, + pluginId: requirement.pluginId, + requirement, + status: this.resolveStatus(requirement, entry) + }); + } + + for (const entry of installedEntries) { + if (!requirementIds.has(entry.manifest.id)) { + comparisons.push({ + installed: entry.manifest, + pluginId: entry.manifest.id, + status: entry.enabled ? 'enabled' : 'disabled' + }); + } + } + + return comparisons.sort((left, right) => left.pluginId.localeCompare(right.pluginId)); + }); + + constructor() { + this.realtime.onSignalingMessage + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((message) => { + if ((message.type === 'plugin_requirements' || message.type === 'plugin_requirements_changed') && isSnapshotMessage(message)) { + this.setSnapshot(message.serverId, message.snapshot); + } + }); + + effect(() => { + const roomId = this.currentRoomId(); + + if (roomId) { + void this.refreshCurrent(); + } + }); + } + + async refreshCurrent(): Promise { + const roomId = this.currentRoomId(); + + if (!roomId) { + return; + } + + try { + const apiBaseUrl = this.serverDirectory.getApiBaseUrl(this.currentRoomSourceSelector()); + const snapshot = await new Promise((resolve, reject) => { + this.pluginRequirements.getSnapshot(apiBaseUrl, roomId).subscribe({ + error: reject, + next: resolve + }); + }); + + this.setSnapshot(roomId, snapshot); + this.refreshErrorsSignal.update((errors) => { + const { [roomId]: _removed, ...next } = errors; + + return next; + }); + } catch (error) { + this.refreshErrorsSignal.update((errors) => ({ + ...errors, + [roomId]: error instanceof Error ? error.message : 'Unable to refresh plugin requirements' + })); + } + } + + comparisonFor(pluginId: string): PluginRequirementComparison | null { + return this.comparisons().find((comparison) => comparison.pluginId === pluginId) ?? null; + } + + dismissOptionalRequirement(requirement: PluginRequirementSummary, options: { persist?: boolean } = {}): void { + const roomId = this.currentRoomId(); + + if (!roomId) { + return; + } + + if (options.persist) { + this.hiddenOptionalSignal.update((dismissals) => { + const nextDismissals = { + ...dismissals, + [roomId]: { + ...(dismissals[roomId] ?? {}), + [requirement.pluginId]: requirement.updatedAt + } + }; + + saveRequirementDismissals(nextDismissals); + return nextDismissals; + }); + + return; + } + + this.sessionDismissedOptionalSignal.update((dismissals) => ({ + ...dismissals, + [roomId]: Array.from(new Set([...(dismissals[roomId] ?? []), requirement.pluginId])) + })); + } + + private setSnapshot(serverId: string, snapshot: PluginRequirementsSnapshot): void { + this.snapshotsSignal.update((snapshots) => ({ + ...snapshots, + [serverId]: snapshot + })); + } + + private currentRoomSourceSelector(): ServerSourceSelector | undefined { + const room = this.currentRoom(); + + if (!room?.sourceId && !room?.sourceUrl) { + return undefined; + } + + return { + sourceId: room.sourceId, + sourceUrl: room.sourceUrl + }; + } + + private resolveStatus( + requirement: PluginRequirementSummary, + entry: { enabled: boolean; manifest: TojuPluginManifest } | undefined + ): PluginRequirementComparisonStatus { + if (requirement.status === 'blocked') { + return 'blockedByServer'; + } + + if (requirement.status === 'incompatible') { + return 'incompatible'; + } + + if (!entry) { + return 'missing'; + } + + if (!entry.enabled) { + return 'disabled'; + } + + if (requirement.versionRange && !isVersionCompatible(entry.manifest.version, requirement.versionRange)) { + return 'incompatible'; + } + + return 'enabled'; + } + + private isMissingInstallableRequirement(comparison: PluginRequirementComparison): boolean { + const requirement = comparison.requirement; + + return comparison.status === 'missing' + && !!requirement + && (requirement.status === 'required' || requirement.status === 'optional' || requirement.status === 'recommended') + && (!!requirement.manifest || !!requirement.installUrl); + } + + private isOptionalRequirementDismissed(requirement: PluginRequirementSummary): boolean { + const roomId = this.currentRoomId(); + + if (!roomId) { + return true; + } + + if ((this.sessionDismissedOptionalSignal()[roomId] ?? []).includes(requirement.pluginId)) { + return true; + } + + const hiddenAt = this.hiddenOptionalSignal()[roomId]?.[requirement.pluginId]; + + return typeof hiddenAt === 'number' && hiddenAt >= requirement.updatedAt; + } +} + +function loadRequirementDismissals(): RequirementDismissalState { + try { + const rawValue = localStorage.getItem(getUserScopedStorageKey(STORAGE_KEY_OPTIONAL_REQUIREMENT_DISMISSALS)); + + if (!rawValue) { + return {}; + } + + return normalizeRequirementDismissals(JSON.parse(rawValue) as unknown); + } catch { + return {}; + } +} + +function saveRequirementDismissals(dismissals: RequirementDismissalState): void { + try { + localStorage.setItem(getUserScopedStorageKey(STORAGE_KEY_OPTIONAL_REQUIREMENT_DISMISSALS), JSON.stringify(dismissals)); + } catch {} +} + +function normalizeRequirementDismissals(value: unknown): RequirementDismissalState { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + + return Object.fromEntries(Object.entries(value as Record) + .map(([serverId, serverValue]) => [serverId, normalizeServerDismissals(serverValue)]) + .filter(([, serverValue]) => Object.keys(serverValue).length > 0)); +} + +function normalizeServerDismissals(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + + return Object.fromEntries(Object.entries(value as Record) + .filter((entry): entry is [string, number] => typeof entry[1] === 'number')); +} + +function isSnapshotMessage(message: unknown): message is { serverId: string; snapshot: PluginRequirementsSnapshot } { + const record = message as Record; + + return typeof record['serverId'] === 'string' + && !!record['snapshot'] + && typeof record['snapshot'] === 'object'; +} + +function isVersionCompatible(version: string, versionRange: string): boolean { + const normalizedRange = versionRange.trim(); + + if (!normalizedRange || normalizedRange === '*') { + return true; + } + + if (normalizedRange.startsWith('^')) { + return version.split('.')[0] === normalizedRange.slice(1).split('.')[0]; + } + + if (normalizedRange.startsWith('~')) { + const [major, minor] = version.split('.'); + const [rangeMajor, rangeMinor] = normalizedRange.slice(1).split('.'); + + return major === rangeMajor && minor === rangeMinor; + } + + return version === normalizedRange; +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-requirement.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-requirement.service.ts new file mode 100644 index 0000000..59bb545 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-requirement.service.ts @@ -0,0 +1,77 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import type { + PluginEventDefinitionSummary, + PluginRequirementStatus, + PluginRequirementSummary, + PluginRequirementsSnapshot, + TojuPluginManifest +} from '../../../../shared-kernel'; + +export interface UpsertPluginRequirementRequest { + actorUserId: string; + installUrl?: string; + manifest?: TojuPluginManifest; + reason?: string; + sourceUrl?: string; + status: PluginRequirementStatus; + versionRange?: string; +} + +export interface UpsertPluginEventDefinitionRequest { + actorUserId: string; + direction: 'clientToServer' | 'serverRelay' | 'p2pHint'; + maxPayloadBytes?: number; + rateLimitJson?: string; + schemaJson?: string; + scope: 'server' | 'channel' | 'user' | 'plugin'; +} + +@Injectable({ providedIn: 'root' }) +export class PluginRequirementService { + private readonly http = inject(HttpClient); + + getSnapshot(apiBaseUrl: string, serverId: string): Observable { + return this.http.get(`${this.apiBase(apiBaseUrl)}/servers/${encodeURIComponent(serverId)}/plugins`); + } + + upsertRequirement( + apiBaseUrl: string, + serverId: string, + pluginId: string, + request: UpsertPluginRequirementRequest + ): Observable<{ requirement: PluginRequirementSummary }> { + return this.http.put<{ requirement: PluginRequirementSummary }>( + `${this.apiBase(apiBaseUrl)}/servers/${encodeURIComponent(serverId)}/plugins/${encodeURIComponent(pluginId)}/requirement`, + request + ); + } + + deleteRequirement(apiBaseUrl: string, serverId: string, pluginId: string, actorUserId: string): Observable<{ ok: boolean }> { + return this.http.delete<{ ok: boolean }>( + `${this.apiBase(apiBaseUrl)}/servers/${encodeURIComponent(serverId)}/plugins/${encodeURIComponent(pluginId)}/requirement`, + { body: { actorUserId } } + ); + } + + upsertEventDefinition( + apiBaseUrl: string, + serverId: string, + pluginId: string, + eventName: string, + request: UpsertPluginEventDefinitionRequest + ): Observable<{ eventDefinition: PluginEventDefinitionSummary }> { + const eventUrl = `${this.apiBase(apiBaseUrl)}/servers/${encodeURIComponent(serverId)}` + + `/plugins/${encodeURIComponent(pluginId)}/events/${encodeURIComponent(eventName)}`; + + return this.http.put<{ eventDefinition: PluginEventDefinitionSummary }>( + eventUrl, + request + ); + } + + private apiBase(apiBaseUrl: string): string { + return apiBaseUrl.replace(/\/$/, ''); + } +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-storage.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-storage.service.ts new file mode 100644 index 0000000..02dcf91 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-storage.service.ts @@ -0,0 +1,150 @@ +import { Injectable, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage'; +import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service'; +import { selectCurrentRoomId } from '../../../../store/rooms/rooms.selectors'; + +const STORAGE_PREFIX_PLUGIN_LOCAL = 'metoyou_plugin_local'; +const STORAGE_PREFIX_PLUGIN_SERVER_LOCAL = 'metoyou_plugin_server_local'; + +type PluginDataScope = 'local' | 'server'; + +@Injectable({ providedIn: 'root' }) +export class PluginStorageService { + private readonly electronBridge = inject(ElectronBridgeService); + private readonly store = inject(Store); + private readonly currentRoomId = this.store.selectSignal(selectCurrentRoomId); + + getLocal(pluginId: string, key: string): unknown { + return this.read(`${STORAGE_PREFIX_PLUGIN_LOCAL}:${pluginId}:${key}`); + } + + removeLocal(pluginId: string, key: string): void { + localStorage.removeItem(getUserScopedStorageKey(`${STORAGE_PREFIX_PLUGIN_LOCAL}:${pluginId}:${key}`)); + void this.deleteFromClientDatabase(pluginId, 'local', key); + } + + setLocal(pluginId: string, key: string, value: unknown): void { + this.write(`${STORAGE_PREFIX_PLUGIN_LOCAL}:${pluginId}:${key}`, value); + void this.writeToClientDatabase(pluginId, 'local', key, value); + } + + async readClientData(pluginId: string, key: string): Promise { + return await this.readScopedData(pluginId, 'local', key); + } + + async removeClientData(pluginId: string, key: string): Promise { + localStorage.removeItem(getUserScopedStorageKey(`${STORAGE_PREFIX_PLUGIN_LOCAL}:${pluginId}:${key}`)); + await this.deleteFromClientDatabase(pluginId, 'local', key); + } + + async writeClientData(pluginId: string, key: string, value: unknown): Promise { + this.write(`${STORAGE_PREFIX_PLUGIN_LOCAL}:${pluginId}:${key}`, value); + await this.writeToClientDatabase(pluginId, 'local', key, value); + } + + async readServerData(pluginId: string, key: string): Promise { + return await this.readScopedData(pluginId, 'server', key, this.requireRoomId()); + } + + async removeServerData(pluginId: string, key: string): Promise { + localStorage.removeItem(getUserScopedStorageKey(this.serverLocalKey(pluginId, key))); + await this.deleteFromClientDatabase(pluginId, 'server', key, this.requireRoomId()); + } + + async writeServerData(pluginId: string, key: string, value: unknown): Promise { + this.write(this.serverLocalKey(pluginId, key), value); + await this.writeToClientDatabase(pluginId, 'server', key, value, this.requireRoomId()); + } + + private async readScopedData(pluginId: string, scope: PluginDataScope, key: string, serverId?: string): Promise { + const api = this.electronBridge.getApi(); + + if (api) { + return await api.query({ + type: 'get-plugin-data', + payload: { + key, + pluginId, + scope, + serverId + } + }); + } + + return this.read(scope === 'server' + ? this.serverLocalKey(pluginId, key) + : `${STORAGE_PREFIX_PLUGIN_LOCAL}:${pluginId}:${key}`); + } + + private async writeToClientDatabase(pluginId: string, scope: PluginDataScope, key: string, value: unknown, serverId?: string): Promise { + const api = this.electronBridge.getApi(); + + if (!api) { + return; + } + + await api.command({ + type: 'save-plugin-data', + payload: { + key, + pluginId, + scope, + serverId, + value + } + }); + } + + private async deleteFromClientDatabase(pluginId: string, scope: PluginDataScope, key: string, serverId?: string): Promise { + const api = this.electronBridge.getApi(); + + if (!api) { + return; + } + + await api.command({ + type: 'delete-plugin-data', + payload: { + key, + pluginId, + scope, + serverId + } + }); + } + + private serverLocalKey(pluginId: string, key: string): string { + const roomId = this.requireRoomId(); + + return `${STORAGE_PREFIX_PLUGIN_SERVER_LOCAL}:${roomId}:${pluginId}:${key}`; + } + + private requireRoomId(): string { + const roomId = this.currentRoomId(); + + if (!roomId) { + throw new Error('No active server for plugin server data'); + } + + return roomId; + } + + private read(key: string): unknown { + const raw = localStorage.getItem(getUserScopedStorageKey(key)); + + if (!raw) { + return null; + } + + try { + return JSON.parse(raw) as unknown; + } catch { + return null; + } + } + + private write(key: string, value: unknown): void { + localStorage.setItem(getUserScopedStorageKey(key), JSON.stringify(value)); + } +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-store.service.spec.ts b/toju-app/src/app/domains/plugins/application/services/plugin-store.service.spec.ts new file mode 100644 index 0000000..deae055 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-store.service.spec.ts @@ -0,0 +1,306 @@ +import { Injector } from '@angular/core'; +import type { TojuPluginManifest } from '../../../../shared-kernel'; +import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service'; +import { PluginStoreService } from './plugin-store.service'; +import { PluginHostService } from './plugin-host.service'; +import { PluginDesktopStateService } from './plugin-desktop-state.service'; +import { PluginRequirementService } from './plugin-requirement.service'; +import { PluginRegistryService } from './plugin-registry.service'; +import type { PluginStoreEntry } from '../../domain/models/plugin-store.models'; + +describe('PluginStoreService', () => { + let fetchMock: ReturnType; + let registerLocalManifest: ReturnType; + let unregister: ReturnType; + let storage: Storage; + + beforeEach(() => { + storage = createMemoryStorage(); + vi.stubGlobal('localStorage', storage); + fetchMock = vi.fn(); + registerLocalManifest = vi.fn((manifest: TojuPluginManifest, sourcePath?: string) => ({ + enabled: true, + manifest, + sourcePath, + state: 'validated', + validationIssues: [] + })); + + unregister = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + }); + + afterEach(() => { + storage.clear(); + vi.unstubAllGlobals(); + }); + + it('loads plugin entries from source manifests and resolves relative links', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ + plugins: [ + { + author: 'Ada Example', + description: 'Adds better channel tools.', + github: 'https://github.com/example/better-channels', + id: 'example.better-channels', + image: './images/better.png', + install: './better/toju-plugin.json', + readme: './better/README.md', + title: 'Better Channels', + version: '1.2.0' + } + ], + title: 'Example Plugins' + })); + + const service = createService(registerLocalManifest, unregister); + + await service.addSourceUrl('https://plugins.example.test/index.json#latest'); + + expect(service.sourceUrls()).toEqual(['https://plugins.example.test/index.json']); + expect(service.sources()[0]?.title).toBe('Example Plugins'); + expect(service.availablePlugins()).toEqual([ + expect.objectContaining({ + author: 'Ada Example', + githubUrl: 'https://github.com/example/better-channels', + id: 'example.better-channels', + imageUrl: 'https://plugins.example.test/images/better.png', + installUrl: 'https://plugins.example.test/better/toju-plugin.json', + readmeUrl: 'https://plugins.example.test/better/README.md', + sourceTitle: 'Example Plugins', + title: 'Better Channels', + version: '1.2.0' + }) + ]); + }); + + it('accepts local source manifest paths and resolves relative file links', async () => { + const localSourceManifest = { + plugins: [ + { + description: 'Local plugin source.', + id: 'example.local-plugin', + image: './icon.svg', + install: './toju-plugin.json', + readme: './README.md', + title: 'Local Plugin', + version: '1.0.0' + } + ], + title: 'Local Plugins' + }; + const readFile = vi.fn(async () => toBase64(JSON.stringify(localSourceManifest))); + const service = createService(registerLocalManifest, unregister, { readFile }); + + await service.addSourceUrl('/home/ludde/Desktop/TestPlugin/plugin-source.json'); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(readFile).toHaveBeenCalledWith('/home/ludde/Desktop/TestPlugin/plugin-source.json'); + expect(service.sourceUrls()).toEqual(['file:///home/ludde/Desktop/TestPlugin/plugin-source.json']); + expect(service.availablePlugins()).toEqual([ + expect.objectContaining({ + id: 'example.local-plugin', + imageUrl: 'file:///home/ludde/Desktop/TestPlugin/icon.svg', + installUrl: 'file:///home/ludde/Desktop/TestPlugin/toju-plugin.json', + readmeUrl: 'file:///home/ludde/Desktop/TestPlugin/README.md', + sourceTitle: 'Local Plugins' + }) + ]); + }); + + it('installs, detects updates, and uninstalls store plugins', async () => { + const manifest = createManifest({ version: '1.0.0' }); + const plugin = createStoreEntry({ version: '1.0.0' }); + + fetchMock.mockResolvedValueOnce(jsonResponse(manifest)); + + const service = createService(registerLocalManifest, unregister); + + await service.installPlugin(plugin); + + expect(registerLocalManifest).toHaveBeenCalledWith(manifest, plugin.installUrl); + expect(service.installedPlugins()[0]?.manifest.id).toBe(plugin.id); + expect(service.getActionLabel(plugin)).toBe('Uninstall'); + expect(service.getActionLabel(createStoreEntry({ version: '1.1.0' }))).toBe('Update'); + + await service.uninstallPlugin(plugin.id); + + expect(unregister).toHaveBeenCalledWith(plugin.id); + expect(service.installedPlugins()).toEqual([]); + }); + + it('caches plugin bundle entrypoints locally before registering installed plugins', async () => { + const manifest = createManifest({ entrypoint: './dist/main.js' }); + const plugin = createStoreEntry({ + bundleUrl: 'https://plugins.example.test/better/bundle.js', + version: '1.0.0' + }); + const electronApi = { + ensureDir: vi.fn(async () => true), + getAppDataPath: vi.fn(async () => '/tmp/metoyou-user-data'), + writeFile: vi.fn(async () => true) + }; + + fetchMock + .mockResolvedValueOnce(jsonResponse(manifest)) + .mockResolvedValueOnce(textResponse('export function activate() {}')); + + const service = createService(registerLocalManifest, unregister, electronApi); + + await service.installPlugin(plugin); + + expect(electronApi.ensureDir).toHaveBeenCalledWith('/tmp/metoyou-user-data/plugin-bundles/example.better-channels/1.0.0'); + expect(electronApi.writeFile).toHaveBeenCalledWith( + '/tmp/metoyou-user-data/plugin-bundles/example.better-channels/1.0.0/main.js', + expect.any(String) + ); + + expect(registerLocalManifest).toHaveBeenCalledWith( + expect.objectContaining({ + bundle: { + entrypoint: './main.js', + url: plugin.bundleUrl + }, + entrypoint: './main.js', + id: manifest.id + }), + 'file:///tmp/metoyou-user-data/plugin-bundles/example.better-channels/1.0.0/toju-plugin.json' + ); + }); + + it('loads plugin readmes as markdown text', async () => { + const plugin = createStoreEntry({ readmeUrl: 'https://plugins.example.test/better/README.md' }); + + fetchMock.mockResolvedValueOnce(textResponse('# Better Channels')); + + const service = createService(registerLocalManifest, unregister); + const readme = await service.loadReadme(plugin); + + expect(readme).toEqual({ + markdown: '# Better Channels', + pluginId: plugin.id, + title: plugin.title, + url: plugin.readmeUrl + }); + }); +}); + +function createService( + registerLocalManifest: ReturnType, + unregister: ReturnType, + electronApi: { + ensureDir?: (dirPath: string) => Promise; + getAppDataPath?: () => Promise; + readFile?: (filePath: string) => Promise; + writeFile?: (filePath: string, data: string) => Promise; + } | null = null +): PluginStoreService { + const injector = Injector.create({ + providers: [ + PluginStoreService, + { + provide: ElectronBridgeService, + useValue: { + getApi: vi.fn(() => electronApi) + } + }, + { + provide: PluginHostService, + useValue: { + activatePersistedPlugins: vi.fn(async () => {}), + deactivatePlugin: vi.fn(async () => {}), + isPluginActive: vi.fn(() => false), + registerLocalManifest + } + }, + { + provide: PluginDesktopStateService, + useValue: { + readJson: vi.fn(async (_key: string, fallback: unknown) => fallback), + writeJson: vi.fn(async () => undefined) + } + }, + { + provide: PluginRegistryService, + useValue: { unregister } + }, + { + provide: PluginRequirementService, + useValue: {} + } + ] + }); + + return injector.get(PluginStoreService); +} + +function toBase64(value: string): string { + return Buffer.from(value, 'utf8').toString('base64'); +} + +function createManifest(overrides: Partial = {}): TojuPluginManifest { + return { + apiVersion: '1.0.0', + compatibility: { + minimumTojuVersion: '1.0.0' + }, + description: 'Adds better channel tools.', + entrypoint: './dist/main.js', + id: 'example.better-channels', + kind: 'client', + schemaVersion: 1, + title: 'Better Channels', + version: '1.0.0', + ...overrides + }; +} + +function createStoreEntry(overrides: Partial = {}): PluginStoreEntry { + return { + author: 'Ada Example', + description: 'Adds better channel tools.', + githubUrl: 'https://github.com/example/better-channels', + id: 'example.better-channels', + imageUrl: 'https://plugins.example.test/images/better.png', + installUrl: 'https://plugins.example.test/better/toju-plugin.json', + readmeUrl: 'https://plugins.example.test/better/README.md', + sourceTitle: 'Example Plugins', + sourceUrl: 'https://plugins.example.test/index.json', + title: 'Better Channels', + version: '1.0.0', + ...overrides + }; +} + +function jsonResponse(value: unknown): Response { + return { + json: vi.fn(async () => value), + ok: true, + status: 200, + text: vi.fn(async () => JSON.stringify(value)) + } as unknown as Response; +} + +function textResponse(value: string): Response { + return { + json: vi.fn(async () => JSON.parse(value) as unknown), + ok: true, + status: 200, + text: vi.fn(async () => value) + } as unknown as Response; +} + +function createMemoryStorage(): Storage { + const values = new Map(); + + return { + get length(): number { + return values.size; + }, + clear: vi.fn(() => values.clear()), + getItem: vi.fn((key: string) => values.get(key) ?? null), + key: vi.fn((index: number) => Array.from(values.keys())[index] ?? null), + removeItem: vi.fn((key: string) => values.delete(key)), + setItem: vi.fn((key: string, value: string) => values.set(key, value)) + }; +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-store.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-store.service.ts new file mode 100644 index 0000000..7413812 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-store.service.ts @@ -0,0 +1,1349 @@ +/* eslint-disable @typescript-eslint/member-ordering */ +import { + DestroyRef, + Injectable, + computed, + effect, + inject, + signal +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Store } from '@ngrx/store'; +import { firstValueFrom } from 'rxjs'; +import { RealtimeSessionFacade } from '../../../../core/realtime'; +import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage'; +import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service'; +import type { + PluginRequirementSummary, + TojuPluginInstallScope, + TojuPluginManifest +} from '../../../../shared-kernel'; +import { + selectCurrentRoom, + selectCurrentRoomId, + selectCurrentRoomName, + selectSavedRooms +} from '../../../../store/rooms/rooms.selectors'; +import { selectCurrentUser } from '../../../../store/users/users.selectors'; +import { ServerDirectoryFacade, type ServerSourceSelector } from '../../../server-directory'; +import { getPluginInstallScope } from '../../domain/logic/plugin-install-scope.logic'; +import { validateTojuPluginManifest } from '../../domain/logic/plugin-manifest-validation.logic'; +import type { + InstalledStorePlugin, + PersistedServerPluginInstallState, + PersistedPluginStoreState, + PluginStoreEntry, + PluginStoreInstallState, + PluginStoreActionLabel, + PluginStoreReadme, + PluginStoreSourceResult +} from '../../domain/models/plugin-store.models'; +import { PluginHostService } from './plugin-host.service'; +import { PluginDesktopStateService } from './plugin-desktop-state.service'; +import { PluginRequirementService } from './plugin-requirement.service'; +import { PluginRegistryService } from './plugin-registry.service'; + +const STORE_SCHEMA_VERSION = 1; +const STORAGE_KEY_PLUGIN_STORE = 'metoyou_plugin_store'; +const STORAGE_KEY_SERVER_PLUGIN_INSTALLS = 'metoyou_server_plugin_installs'; +const PLUGIN_CACHE_DIR = 'plugin-bundles'; +const DEFAULT_STORE_STATE: PersistedPluginStoreState = { + installedPlugins: [], + sourceUrls: [] +}; + +export interface PluginStoreInstallOptions { + activate?: boolean; + manifest?: TojuPluginManifest; + optional?: boolean; + serverId?: string; +} + +interface ServerInstalledPluginsLoadState { + actorUserId: string | null; + loaded: boolean; + loading: boolean; + roomId: string | null; +} + +@Injectable({ providedIn: 'root' }) +export class PluginStoreService { + private readonly electronBridge = inject(ElectronBridgeService); + private readonly desktopState = inject(PluginDesktopStateService); + private readonly destroyRef = inject(DestroyRef); + private readonly host = inject(PluginHostService); + private readonly pluginRequirements = inject(PluginRequirementService); + private readonly realtime = inject(RealtimeSessionFacade, { optional: true }); + private readonly registry = inject(PluginRegistryService); + private readonly serverDirectory = inject(ServerDirectoryFacade, { optional: true }); + private readonly store = inject(Store, { optional: true }); + private readonly currentRoom = this.store?.selectSignal(selectCurrentRoom) ?? null; + private readonly currentRoomId = this.store?.selectSignal(selectCurrentRoomId) ?? null; + private readonly currentRoomName = this.store?.selectSignal(selectCurrentRoomName) ?? null; + private readonly savedRooms = this.store?.selectSignal(selectSavedRooms) ?? null; + private readonly currentUser = this.store?.selectSignal(selectCurrentUser) ?? null; + private readonly sourceUrlsSignal = signal([]); + private readonly sourcesSignal = signal([]); + private readonly clientInstalledPluginsSignal = signal([]); + private readonly serverInstalledPluginsSignal = signal([]); + private readonly serverInstalledPluginsLoadStateSignal = signal({ + actorUserId: null, + loaded: false, + loading: false, + roomId: null + }); + private readonly loadingSignal = signal(false); + private refreshAbortController: AbortController | null = null; + private refreshVersion = 0; + private installedLoadVersion = 0; + private autoUpdateInProgress = false; + private stateMutated = false; + + readonly sourceUrls = this.sourceUrlsSignal.asReadonly(); + readonly sources = this.sourcesSignal.asReadonly(); + readonly installedPlugins = computed(() => { + const installedPlugins = this.clientInstalledPluginsSignal().concat(this.serverInstalledPluginsSignal()); + + return installedPlugins.sort(sortInstalledPlugins); + }); + readonly isLoading = this.loadingSignal.asReadonly(); + readonly availablePlugins = computed(() => this.sources().flatMap((source) => source.plugins)); + readonly hasActiveServerInstallScope = computed(() => !!this.currentRoomId?.()); + readonly installedById = computed(() => new Map(this.installedPlugins().map((plugin) => [plugin.manifest.id, plugin]))); + readonly installScopeLabel = computed(() => this.currentRoomName?.() || 'this device'); + readonly serverInstalledPluginsReadyForCurrentRoom = computed(() => { + const roomId = this.currentRoomId?.() ?? null; + const actorUserId = this.currentActorUserId(); + + if (!roomId || !actorUserId || !this.serverDirectory) { + return true; + } + + const loadState = this.serverInstalledPluginsLoadStateSignal(); + + return loadState.loaded + && !loadState.loading + && loadState.roomId === roomId + && loadState.actorUserId === actorUserId; + }); + + constructor() { + const state = this.loadState(); + + this.sourceUrlsSignal.set(state.sourceUrls); + void this.applyInstalledPlugins(state.installedPlugins, 'client'); + + if (state.sourceUrls.length > 0) { + void this.refreshSources(); + } + + if (this.currentRoomId && this.currentUser && this.serverDirectory) { + effect(() => { + const roomId = this.currentRoomId?.() ?? null; + const actorUserId = this.currentActorUserId(); + + void this.loadInstalledPluginsForScope(roomId, actorUserId); + }); + + this.realtime?.onSignalingMessage + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((message) => { + if (isPluginRequirementsChangedMessage(message) && message.serverId === this.currentRoomId?.()) { + void this.loadInstalledPluginsForScope(message.serverId, this.currentActorUserId()); + } + }); + } + + void this.loadDesktopState(); + } + + async addSourceUrl(rawUrl: string): Promise { + const sourceUrl = normalizeSourceUrl(rawUrl, 'Plugin source URL'); + + if (this.sourceUrls().includes(sourceUrl)) { + throw new Error('Plugin source already exists'); + } + + this.sourceUrlsSignal.update((sourceUrls) => [...sourceUrls, sourceUrl]); + this.saveState(); + await this.refreshSources(); + } + + async removeSourceUrl(sourceUrl: string): Promise { + this.sourceUrlsSignal.update((sourceUrls) => sourceUrls.filter((candidate) => candidate !== sourceUrl)); + this.sourcesSignal.update((sources) => sources.filter((source) => source.url !== sourceUrl)); + this.saveState(); + await this.refreshSources(); + } + + async refreshSources(): Promise { + const currentRefresh = this.refreshVersion + 1; + const abortController = new AbortController(); + + this.refreshVersion = currentRefresh; + this.refreshAbortController?.abort(); + this.refreshAbortController = abortController; + this.loadingSignal.set(true); + + try { + const sources = await Promise.all(this.sourceUrls().map((sourceUrl) => this.loadSource(sourceUrl, abortController.signal))); + + if (this.refreshVersion === currentRefresh) { + this.sourcesSignal.set(sources); + void this.autoUpdateInstalledPlugins(); + } + } finally { + if (this.refreshVersion === currentRefresh) { + this.refreshAbortController = null; + this.loadingSignal.set(false); + } + } + } + + async installPlugin(plugin: PluginStoreEntry, options: PluginStoreInstallOptions = {}): Promise { + if (!plugin.installUrl) { + throw new Error('Plugin does not provide an install manifest URL'); + } + + const manifest = this.withStoreBundleMetadata(options.manifest ?? await this.fetchPluginManifest(plugin.installUrl), plugin); + const installScope = getPluginInstallScope(manifest); + const targetServerId = this.resolveInstallTargetServerId(installScope, options.serverId); + const now = Date.now(); + const currentScopePlugins = installScope === 'server' + ? await this.installedPluginsForServer(targetServerId) + : this.installedPluginsForScope(installScope); + const existing = currentScopePlugins.find((candidate) => candidate.manifest.id === manifest.id); + const installOptions = { + ...options, + activate: options.activate === true || (installScope === 'server' && !existing) + }; + const installedPlugin = await this.cacheInstalledPlugin({ + bundleUrl: manifest.bundle?.url ?? plugin.bundleUrl, + installedAt: existing?.installedAt ?? now, + installUrl: plugin.installUrl, + manifest, + sourceUrl: plugin.sourceUrl, + updatedAt: now + }); + const nextInstalledPlugins = currentScopePlugins + .filter((candidate) => candidate.manifest.id !== manifest.id) + .concat(installedPlugin) + .sort(sortInstalledPlugins); + + await this.persistInstallResult(installScope, targetServerId, nextInstalledPlugins, installedPlugin, installOptions); + await this.registerInstallResult(installScope, targetServerId, nextInstalledPlugins, installedPlugin, installOptions); + + return installedPlugin; + } + + private resolveInstallTargetServerId(installScope: TojuPluginInstallScope, requestedServerId: string | undefined): string | null { + if (installScope !== 'server') { + return null; + } + + const targetServerId = requestedServerId ?? this.currentRoomId?.() ?? null; + + if (!targetServerId) { + throw new Error('Open a chat server before installing server-scoped plugins'); + } + + return targetServerId; + } + + private async persistInstallResult( + installScope: TojuPluginInstallScope, + targetServerId: string | null, + nextInstalledPlugins: InstalledStorePlugin[], + installedPlugin: InstalledStorePlugin, + options: PluginStoreInstallOptions + ): Promise { + if (installScope === 'server') { + await this.saveServerPluginRequirement(installedPlugin, targetServerId, options.optional === true ? 'optional' : 'required'); + return; + } + + await this.persistInstalledPlugins(nextInstalledPlugins, installScope); + } + + private async registerInstallResult( + installScope: TojuPluginInstallScope, + targetServerId: string | null, + nextInstalledPlugins: InstalledStorePlugin[], + installedPlugin: InstalledStorePlugin, + options: PluginStoreInstallOptions + ): Promise { + if (installScope === 'server' && targetServerId) { + await this.writeLocalServerInstalledPlugins(targetServerId, nextInstalledPlugins); + } + + if (installScope === 'server' && options.activate) { + this.registry.setEnabled(installedPlugin.manifest.id, true); + } + + if (installScope !== 'client' && targetServerId !== this.currentRoomId?.()) { + if (options.activate) { + await this.host.rememberActivation(installedPlugin.manifest.id); + } + + return; + } + + this.host.registerLocalManifest(installedPlugin.manifest, installedPlugin.cachedSourcePath ?? installedPlugin.installUrl); + + this.setInstalledPluginsForScope(installScope, nextInstalledPlugins); + + if (options.activate) { + await this.host.activatePluginById(installedPlugin.manifest.id); + } + } + + async loadInstallManifest(plugin: PluginStoreEntry): Promise { + if (!plugin.installUrl) { + throw new Error('Plugin does not provide an install manifest URL'); + } + + return await this.fetchPluginManifest(plugin.installUrl); + } + + async uninstallPlugin(pluginId: string, scope?: TojuPluginInstallScope, options: { serverId?: string } = {}): Promise { + const installScope = scope ?? this.findInstalledPluginScope(pluginId) ?? 'client'; + const currentInstalledPlugins = installScope === 'server' + ? await this.installedPluginsForServer(options.serverId ?? this.currentRoomId?.() ?? null) + : this.installedPluginsForScope(installScope); + const nextInstalledPlugins = currentInstalledPlugins.filter((installedPlugin) => installedPlugin.manifest.id !== pluginId); + + if (installScope === 'server') { + await this.deleteServerPluginRequirement(pluginId, options.serverId); + } else { + await this.persistInstalledPlugins(nextInstalledPlugins, installScope); + } + + if (installScope === 'server' && options.serverId && options.serverId !== this.currentRoomId?.()) { + return; + } + + await this.host.deactivatePlugin(pluginId, { forgetActivation: true }); + this.registry.unregister(pluginId); + this.setInstalledPluginsForScope(installScope, nextInstalledPlugins); + } + + async loadInstalledPluginsForServer(serverId: string): Promise { + return await this.installedPluginsForServer(serverId); + } + + async getLocalServerInstalledPluginIds(serverId: string): Promise> { + const installedPlugins = await this.readLocalServerInstalledPlugins(serverId); + + return new Set(installedPlugins.map((installedPlugin) => installedPlugin.manifest.id)); + } + + async installServerRequirementsLocally( + serverId: string, + requirements: PluginRequirementSummary[], + options: { activate?: boolean } = {} + ): Promise { + const installedPlugins: InstalledStorePlugin[] = []; + + for (const requirement of requirements) { + const installedPlugin = await this.resolveLocalInstallFromRequirement(requirement); + + if (installedPlugin) { + installedPlugins.push(installedPlugin); + } + } + + if (installedPlugins.length === 0) { + return await this.readLocalServerInstalledPlugins(serverId); + } + + const currentInstalledPlugins = await this.readLocalServerInstalledPlugins(serverId); + const currentById = new Map(currentInstalledPlugins.map((installedPlugin) => [installedPlugin.manifest.id, installedPlugin])); + const nextById = new Map(currentById); + + for (const installedPlugin of installedPlugins) { + const existing = currentById.get(installedPlugin.manifest.id); + const cachedPlugin = await this.cacheInstalledPlugin({ + ...installedPlugin, + installedAt: existing?.installedAt ?? installedPlugin.installedAt, + updatedAt: installedPlugin.updatedAt + }); + + nextById.set(cachedPlugin.manifest.id, cachedPlugin); + } + + const nextInstalledPlugins = Array.from(nextById.values()).sort(sortInstalledPlugins); + + if (options.activate) { + for (const installedPlugin of installedPlugins) { + this.registry.setEnabled(installedPlugin.manifest.id, true); + await this.host.rememberActivation(installedPlugin.manifest.id); + } + } + + await this.writeLocalServerInstalledPlugins(serverId, nextInstalledPlugins); + + if (serverId === this.currentRoomId?.()) { + await this.applyInstalledPlugins(nextInstalledPlugins, 'server'); + } + + return nextInstalledPlugins; + } + + async loadReadme(plugin: PluginStoreEntry): Promise { + if (!plugin.readmeUrl) { + throw new Error('Plugin does not provide a readme URL'); + } + + return { + markdown: await this.fetchText(plugin.readmeUrl, 'text/markdown,text/plain,*/*'), + pluginId: plugin.id, + title: plugin.title, + url: plugin.readmeUrl + }; + } + + getInstallState(plugin: PluginStoreEntry): PluginStoreInstallState { + const installed = this.installedPluginForScope(plugin.id, getStoreEntryInstallScope(plugin)); + + if (!installed) { + return 'notInstalled'; + } + + return compareVersions(plugin.version, installed.manifest.version) > 0 + ? 'updateAvailable' + : 'installed'; + } + + getActionLabel(plugin: PluginStoreEntry): PluginStoreActionLabel { + const state = this.getInstallState(plugin); + const serverScoped = getStoreEntryInstallScope(plugin) === 'server'; + + if (state === 'updateAvailable') { + return serverScoped ? 'Update Server' : 'Update'; + } + + if (state === 'installed') { + return serverScoped ? 'Remove from Server' : 'Uninstall'; + } + + return serverScoped ? 'Install to Server' : 'Install'; + } + + canInstallPlugin(plugin: PluginStoreEntry): boolean { + return getStoreEntryInstallScope(plugin) !== 'server' || this.hasActiveServerInstallScope(); + } + + private async loadSource(sourceUrl: string, signal: AbortSignal): Promise { + try { + const sourceValue = await this.fetchJson(sourceUrl, signal); + + return parsePluginSource(sourceUrl, sourceValue); + } catch (error) { + return { + error: error instanceof Error ? error.message : 'Unable to load plugin source', + plugins: [], + url: sourceUrl + }; + } + } + + private async fetchPluginManifest(manifestUrl: string): Promise { + const manifestValue = await this.fetchJson(manifestUrl); + const validation = validateTojuPluginManifest(manifestValue); + + if (!validation.manifest) { + throw new Error(validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join('\n')); + } + + return validation.manifest; + } + + private async fetchJson(url: string, signal?: AbortSignal): Promise { + return JSON.parse(await this.fetchText(url, 'application/json', signal)) as unknown; + } + + private async fetchText(url: string, accept: string, signal?: AbortSignal): Promise { + if (url.startsWith('file://')) { + return await this.readLocalFileUrl(url); + } + + const response = await fetch(url, { headers: { Accept: accept }, signal }); + + if (!response.ok) { + throw new Error(`Request returned ${response.status}`); + } + + return await response.text(); + } + + private async readLocalFileUrl(fileUrl: string): Promise { + const api = this.electronBridge.getApi(); + + if (!api) { + throw new Error('Local plugin source paths require the desktop app'); + } + + const base64Data = await api.readFile(fileUrlToPath(fileUrl)); + const bytes = Uint8Array.from(atob(base64Data), (character) => character.charCodeAt(0)); + + return new TextDecoder().decode(bytes); + } + + private withStoreBundleMetadata(manifest: TojuPluginManifest, plugin: PluginStoreEntry): TojuPluginManifest { + if (!plugin.bundleUrl || manifest.bundle?.url) { + return manifest; + } + + return { + ...manifest, + bundle: { + entrypoint: './main.js', + url: plugin.bundleUrl + } + }; + } + + private async cacheInstalledPlugin(installedPlugin: InstalledStorePlugin): Promise { + if (installedPlugin.cachedSourcePath) { + return installedPlugin; + } + + const api = this.electronBridge.getApi(); + const entrypointSourceUrl = this.resolvePluginBundleSourceUrl(installedPlugin); + const cachedEntrypoint = this.resolveCachedEntrypointPath(installedPlugin.manifest); + + if (!api || !entrypointSourceUrl || !cachedEntrypoint) { + return installedPlugin; + } + + const cachedManifest = this.toCachedRuntimeManifest(installedPlugin.manifest, cachedEntrypoint); + const appDataPath = await api.getAppDataPath(); + const pluginCacheDir = joinLocalPath( + appDataPath, + PLUGIN_CACHE_DIR, + sanitizePathSegment(installedPlugin.manifest.id), + sanitizePathSegment(installedPlugin.manifest.version) + ); + const manifestPath = joinLocalPath(pluginCacheDir, 'toju-plugin.json'); + const entrypointPath = joinLocalPath(pluginCacheDir, cachedEntrypoint); + const cacheRootUrl = localPathToFileUrl(manifestPath); + + if (!cacheRootUrl) { + return installedPlugin; + } + + await api.ensureDir(dirnameLocalPath(entrypointPath)); + await api.writeFile(entrypointPath, bytesToBase64(new TextEncoder().encode(await this.fetchText(entrypointSourceUrl, 'text/javascript,*/*')))); + await api.writeFile(manifestPath, bytesToBase64(new TextEncoder().encode(JSON.stringify(cachedManifest, null, 2)))); + + return { + ...installedPlugin, + bundleUrl: installedPlugin.bundleUrl ?? installedPlugin.manifest.bundle?.url, + cachedAt: Date.now(), + cachedSourcePath: cacheRootUrl, + manifest: cachedManifest + }; + } + + private toCachedRuntimeManifest(manifest: TojuPluginManifest, cachedEntrypoint: string): TojuPluginManifest { + if (!manifest.bundle?.url) { + return manifest; + } + + return { + ...manifest, + entrypoint: cachedEntrypoint.startsWith('./') ? cachedEntrypoint : `./${cachedEntrypoint}` + }; + } + + private resolvePluginBundleSourceUrl(installedPlugin: InstalledStorePlugin): string | null { + const bundleUrl = installedPlugin.bundleUrl ?? installedPlugin.manifest.bundle?.url; + + if (bundleUrl) { + return bundleUrl; + } + + const entrypoint = installedPlugin.manifest.entrypoint; + + if (!entrypoint || !installedPlugin.installUrl || isAbsolutePluginUrl(entrypoint)) { + return null; + } + + return resolveOptionalUrl(installedPlugin.installUrl, entrypoint) ?? null; + } + + private resolveCachedEntrypointPath(manifest: TojuPluginManifest): string | null { + const entrypoint = manifest.bundle?.url + ? manifest.bundle.entrypoint ?? './main.js' + : manifest.entrypoint; + + if (!entrypoint || isAbsolutePluginUrl(entrypoint)) { + return null; + } + + const normalized = entrypoint.replace(/^\.\//, '').replace(/\\/g, '/'); + + if (!normalized || normalized.startsWith('/') || normalized.split('/').includes('..')) { + return null; + } + + return normalized; + } + + private async applyInstalledPlugins(installedPlugins: InstalledStorePlugin[], scope: TojuPluginInstallScope): Promise { + const usableInstalledPlugins: InstalledStorePlugin[] = []; + const scopedInstalledPlugins = installedPlugins.filter((installedPlugin) => getPluginInstallScope(installedPlugin.manifest) === scope); + const nextIds = new Set(scopedInstalledPlugins.map((installedPlugin) => installedPlugin.manifest.id)); + + for (const previousPlugin of this.installedPluginsForScope(scope)) { + if (!nextIds.has(previousPlugin.manifest.id)) { + await this.host.deactivatePlugin(previousPlugin.manifest.id); + this.registry.unregister(previousPlugin.manifest.id); + } + } + + for (const installedPlugin of scopedInstalledPlugins) { + try { + const cachedPlugin = await this.cacheInstalledPlugin(installedPlugin); + + this.host.registerLocalManifest(cachedPlugin.manifest, cachedPlugin.cachedSourcePath ?? cachedPlugin.installUrl); + usableInstalledPlugins.push(cachedPlugin); + } catch { + // Corrupt persisted manifests are ignored so the store can recover on next install. + } + } + + this.setInstalledPluginsForScope(scope, usableInstalledPlugins); + + if (scope === 'server') { + await this.activateServerPlugins(usableInstalledPlugins); + } else { + await this.host.activatePersistedPlugins(); + } + + if (usableInstalledPlugins.length !== scopedInstalledPlugins.length) { + if (scope === 'client') { + await this.persistInstalledPlugins(usableInstalledPlugins, scope); + } + } else if ( + scope === 'client' + && usableInstalledPlugins.some((plugin, index) => plugin.cachedSourcePath !== scopedInstalledPlugins[index]?.cachedSourcePath) + ) { + await this.persistInstalledPlugins(usableInstalledPlugins, scope); + } + } + + private async activateServerPlugins(installedPlugins: InstalledStorePlugin[]): Promise { + for (const installedPlugin of installedPlugins) { + await this.host.activatePluginById(installedPlugin.manifest.id); + } + } + + private async autoUpdateInstalledPlugins(): Promise { + if (this.autoUpdateInProgress || this.sources().length === 0) { + return; + } + + this.autoUpdateInProgress = true; + + try { + await this.autoUpdateScope('client'); + + if (this.currentRoomId?.()) { + await this.autoUpdateScope('server'); + } + } finally { + this.autoUpdateInProgress = false; + } + } + + private async autoUpdateScope(scope: TojuPluginInstallScope): Promise { + for (const installedPlugin of this.installedPluginsForScope(scope)) { + const update = this.findUpdateCandidate(installedPlugin, scope); + + if (!update) { + continue; + } + + try { + await this.installPlugin(update, { + activate: this.host.isPluginActive(installedPlugin.manifest.id), + serverId: scope === 'server' ? this.currentRoomId?.() ?? undefined : undefined + }); + } catch {} + } + } + + private findUpdateCandidate(installedPlugin: InstalledStorePlugin, scope: TojuPluginInstallScope): PluginStoreEntry | null { + const candidates = this.availablePlugins().filter((plugin) => { + return plugin.id === installedPlugin.manifest.id + && getStoreEntryInstallScope(plugin) === scope + && (!installedPlugin.sourceUrl || plugin.sourceUrl === installedPlugin.sourceUrl); + }); + + return candidates + .filter((plugin) => compareVersions(plugin.version, installedPlugin.manifest.version) > 0) + .sort((left, right) => compareVersions(right.version, left.version))[0] ?? null; + } + + private async loadInstalledPluginsForScope(roomId: string | null, actorUserId: string | null): Promise { + const currentLoad = this.installedLoadVersion + 1; + + this.installedLoadVersion = currentLoad; + this.serverInstalledPluginsLoadStateSignal.set({ + actorUserId, + loaded: false, + loading: true, + roomId + }); + + await Promise.resolve(); + + if (!roomId || !actorUserId || !this.serverDirectory) { + if (this.installedLoadVersion === currentLoad) { + await this.applyInstalledPlugins([], 'server'); + this.serverInstalledPluginsLoadStateSignal.set({ + actorUserId, + loaded: true, + loading: false, + roomId + }); + } + + return; + } + + try { + const installedPlugins = await this.readLocalServerInstalledPlugins(roomId); + + if (this.installedLoadVersion === currentLoad && this.currentRoomId?.() === roomId) { + await this.applyInstalledPlugins(installedPlugins, 'server'); + this.serverInstalledPluginsLoadStateSignal.set({ + actorUserId, + loaded: true, + loading: false, + roomId + }); + } + } catch { + if (this.installedLoadVersion === currentLoad && this.currentRoomId?.() === roomId) { + await this.applyInstalledPlugins([], 'server'); + this.serverInstalledPluginsLoadStateSignal.set({ + actorUserId, + loaded: true, + loading: false, + roomId + }); + } + } + } + + private async persistInstalledPlugins( + installedPlugins: InstalledStorePlugin[], + scope: TojuPluginInstallScope, + serverId?: string | null + ): Promise { + const roomId = serverId ?? this.currentRoomId?.() ?? null; + const actorUserId = this.currentActorUserId(); + + if (scope === 'server') { + if (!roomId || !actorUserId || !this.serverDirectory) { + throw new Error('Open a chat server before saving server-scoped plugins'); + } + + await Promise.all(installedPlugins.map((installedPlugin) => this.saveServerPluginRequirement(installedPlugin, roomId, 'required'))); + return; + } + + this.clientInstalledPluginsSignal.set(installedPlugins); + this.saveState(); + } + + private async readServerInstalledPlugins(roomId: string): Promise { + if (!this.serverDirectory) { + return []; + } + + const snapshot = await firstValueFrom(this.pluginRequirements.getSnapshot(this.getPluginApiBaseUrl(roomId), roomId)); + + return snapshot.requirements + .map((requirement) => installedPluginFromRequirement(requirement)) + .filter((installedPlugin): installedPlugin is InstalledStorePlugin => !!installedPlugin) + .sort(sortInstalledPlugins); + } + + private async resolveLocalInstallFromRequirement(requirement: PluginRequirementSummary): Promise { + const existingPlugin = installedPluginFromRequirement(requirement, { includeOptional: true }); + + if (existingPlugin) { + return existingPlugin; + } + + if (!requirement.installUrl) { + return null; + } + + const manifest = await this.fetchPluginManifest(requirement.installUrl); + + if (getPluginInstallScope(manifest) !== 'server') { + return null; + } + + return { + bundleUrl: manifest.bundle?.url, + installedAt: requirement.updatedAt, + installUrl: requirement.installUrl, + manifest, + sourceUrl: requirement.sourceUrl, + updatedAt: requirement.updatedAt + }; + } + + private async readLocalServerInstalledPlugins(serverId: string): Promise { + const state = await this.desktopState.readJson(STORAGE_KEY_SERVER_PLUGIN_INSTALLS, {}); + const normalized = normalizePersistedServerPluginInstallState(state); + + return normalized.servers[serverId] ?? []; + } + + private async writeLocalServerInstalledPlugins(serverId: string, installedPlugins: InstalledStorePlugin[]): Promise { + const state = await this.desktopState.readJson(STORAGE_KEY_SERVER_PLUGIN_INSTALLS, {}); + const normalized = normalizePersistedServerPluginInstallState(state); + const nextServers = installedPlugins.length === 0 + ? Object.fromEntries(Object.entries(normalized.servers).filter(([candidateServerId]) => candidateServerId !== serverId)) + : { + ...normalized.servers, + [serverId]: installedPlugins + }; + + await this.desktopState.writeJson(STORAGE_KEY_SERVER_PLUGIN_INSTALLS, { + schemaVersion: STORE_SCHEMA_VERSION, + servers: nextServers + }); + } + + private async saveServerPluginRequirement( + installedPlugin: InstalledStorePlugin, + roomId: string | null, + status: 'optional' | 'required' + ): Promise { + const actorUserId = this.currentActorUserId(); + + if (!roomId || !actorUserId || !this.serverDirectory) { + throw new Error('Open a chat server before saving server-scoped plugins'); + } + + await firstValueFrom(this.pluginRequirements.upsertRequirement( + this.getPluginApiBaseUrl(roomId), + roomId, + installedPlugin.manifest.id, + { + actorUserId, + installUrl: installedPlugin.installUrl, + manifest: installedPlugin.manifest, + reason: installedPlugin.manifest.description, + sourceUrl: installedPlugin.sourceUrl, + status, + versionRange: `^${installedPlugin.manifest.version}` + } + )); + } + + private async deleteServerPluginRequirement(pluginId: string, serverId?: string): Promise { + const roomId = serverId ?? this.currentRoomId?.() ?? null; + const actorUserId = this.currentActorUserId(); + + if (!roomId || !actorUserId || !this.serverDirectory) { + throw new Error('Open a chat server before removing server-scoped plugins'); + } + + await firstValueFrom(this.pluginRequirements.deleteRequirement(this.getPluginApiBaseUrl(roomId), roomId, pluginId, actorUserId)); + } + + private getPluginApiBaseUrl(serverId: string): string { + const selector = this.serverSourceSelector(serverId); + + return this.serverDirectory?.getApiBaseUrl(selector) ?? ''; + } + + private serverSourceSelector(serverId: string): ServerSourceSelector | undefined { + if (serverId === this.currentRoomId?.()) { + return this.currentRoomSourceSelector(); + } + + const room = this.savedRooms?.().find((candidate) => candidate.id === serverId) ?? null; + + if (!room?.sourceId && !room?.sourceUrl) { + return undefined; + } + + return { + sourceId: room.sourceId, + sourceUrl: room.sourceUrl + }; + } + + private currentRoomSourceSelector(): ServerSourceSelector | undefined { + const room = this.currentRoom?.() ?? null; + + if (!room?.sourceId && !room?.sourceUrl) { + return undefined; + } + + return { + sourceId: room.sourceId, + sourceUrl: room.sourceUrl + }; + } + + private currentActorUserId(): string | null { + const user = this.currentUser?.() ?? null; + + return user?.oderId || user?.id || null; + } + + private async installedPluginsForServer(serverId: string | null): Promise { + if (!serverId) { + return []; + } + + const actorUserId = this.currentActorUserId(); + + if (!actorUserId || !this.serverDirectory) { + throw new Error('Unable to read server plugins without an active user and server directory'); + } + + try { + return await this.readServerInstalledPlugins(serverId); + } catch { + return []; + } + } + + private loadState(): PersistedPluginStoreState { + try { + const raw = localStorage.getItem(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE)); + + if (!raw) { + return { ...DEFAULT_STORE_STATE }; + } + + return normalizePersistedState(JSON.parse(raw) as unknown); + } catch { + return { ...DEFAULT_STORE_STATE }; + } + } + + private saveState(): void { + this.stateMutated = true; + + const state = { + installedPlugins: this.clientInstalledPluginsSignal(), + schemaVersion: STORE_SCHEMA_VERSION, + sourceUrls: this.sourceUrls() + }; + + try { + localStorage.setItem(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE), JSON.stringify(state)); + } catch {} + + void this.desktopState.writeJson(STORAGE_KEY_PLUGIN_STORE, state); + } + + private async loadDesktopState(): Promise { + const state = await this.desktopState.readJson(STORAGE_KEY_PLUGIN_STORE, this.loadState()); + + if (this.stateMutated) { + return; + } + + const normalized = normalizePersistedState(state); + const sourceUrlsChanged = JSON.stringify(normalized.sourceUrls) !== JSON.stringify(this.sourceUrls()); + + if (sourceUrlsChanged) { + this.sourceUrlsSignal.set(normalized.sourceUrls); + void this.refreshSources(); + } + + await this.applyInstalledPlugins(normalized.installedPlugins, 'client'); + } + + private installedPluginsForScope(scope: TojuPluginInstallScope): InstalledStorePlugin[] { + return scope === 'server' ? this.serverInstalledPluginsSignal() : this.clientInstalledPluginsSignal(); + } + + private setInstalledPluginsForScope(scope: TojuPluginInstallScope, installedPlugins: InstalledStorePlugin[]): void { + if (scope === 'server') { + this.serverInstalledPluginsSignal.set(installedPlugins); + return; + } + + this.clientInstalledPluginsSignal.set(installedPlugins); + } + + private installedPluginForScope(pluginId: string, scope: TojuPluginInstallScope): InstalledStorePlugin | undefined { + return this.installedPluginsForScope(scope).find((installedPlugin) => installedPlugin.manifest.id === pluginId); + } + + private findInstalledPluginScope(pluginId: string): TojuPluginInstallScope | null { + if (this.serverInstalledPluginsSignal().some((installedPlugin) => installedPlugin.manifest.id === pluginId)) { + return 'server'; + } + + if (this.clientInstalledPluginsSignal().some((installedPlugin) => installedPlugin.manifest.id === pluginId)) { + return 'client'; + } + + return null; + } +} + +function isPluginRequirementsChangedMessage(message: unknown): message is { serverId: string; type: string } { + if (!isRecord(message)) { + return false; + } + + return (message['type'] === 'plugin_requirements' || message['type'] === 'plugin_requirements_changed') + && typeof message['serverId'] === 'string'; +} + +function installedPluginFromRequirement( + requirement: PluginRequirementSummary, + options: { includeOptional?: boolean } = {} +): InstalledStorePlugin | null { + if (requirement.status === 'blocked' || requirement.status === 'incompatible') { + return null; + } + + if (requirement.status === 'optional' && options.includeOptional !== true) { + return null; + } + + const manifest = requirement.manifest; + + if (!manifest || !isInstalledStorePlugin({ manifest })) { + return null; + } + + return { + bundleUrl: manifest.bundle?.url, + installedAt: requirement.updatedAt, + installUrl: requirement.installUrl, + manifest, + sourceUrl: requirement.sourceUrl, + updatedAt: requirement.updatedAt + }; +} + +function parsePluginSource(sourceUrl: string, sourceValue: unknown): PluginStoreSourceResult { + const sourceRecord = isRecord(sourceValue) ? sourceValue : {}; + const sourceTitle = readString(sourceRecord, 'title', 'name') ?? new URL(sourceUrl).hostname; + const rawPlugins = Array.isArray(sourceValue) + ? sourceValue + : Array.isArray(sourceRecord['plugins']) + ? sourceRecord['plugins'] + : Array.isArray(sourceRecord['items']) + ? sourceRecord['items'] + : []; + const plugins = rawPlugins + .map((entry) => parsePluginEntry(sourceUrl, sourceTitle, entry)) + .filter((entry): entry is PluginStoreEntry => !!entry) + .sort((left, right) => left.title.localeCompare(right.title)); + + return { + loadedAt: Date.now(), + plugins, + title: sourceTitle, + url: sourceUrl + }; +} + +function sortInstalledPlugins(left: InstalledStorePlugin, right: InstalledStorePlugin): number { + return left.manifest.title.localeCompare(right.manifest.title); +} + +function parsePluginEntry(sourceUrl: string, sourceTitle: string, value: unknown): PluginStoreEntry | null { + if (!isRecord(value)) { + return null; + } + + const id = readString(value, 'id', 'pluginId'); + const version = readString(value, 'version') ?? '0.0.0'; + + if (!id) { + return null; + } + + return { + author: readAuthor(value), + bundleUrl: resolveOptionalUrl(sourceUrl, readString(value, 'bundle', 'bundleUrl')), + description: readString(value, 'description', 'summary') ?? '', + githubUrl: resolveOptionalUrl(sourceUrl, readGithubUrl(value)), + homepageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'homepage', 'homepageUrl', 'website')), + id, + imageUrl: resolveOptionalUrl(sourceUrl, readString(value, 'image', 'imageUrl', 'icon', 'iconUrl', 'banner')), + installUrl: resolveOptionalUrl(sourceUrl, readString(value, 'install', 'installUrl', 'manifest', 'manifestUrl')), + readmeUrl: resolveOptionalUrl(sourceUrl, readString(value, 'readme', 'readmeUrl')), + scope: readPluginInstallScope(value), + sourceTitle, + sourceUrl, + title: readString(value, 'title', 'name') ?? id, + version + }; +} + +function getStoreEntryInstallScope(plugin: PluginStoreEntry): TojuPluginInstallScope { + return plugin.scope === 'server' ? 'server' : 'client'; +} + +function readPluginInstallScope(record: Record): TojuPluginInstallScope | undefined { + const scope = readString(record, 'scope', 'installScope', 'pluginScope'); + + return scope === 'server' || scope === 'client' ? scope : undefined; +} + +function normalizePersistedState(value: unknown): PersistedPluginStoreState { + if (!isRecord(value)) { + return { ...DEFAULT_STORE_STATE }; + } + + return { + installedPlugins: Array.isArray(value['installedPlugins']) + ? value['installedPlugins'].filter(isInstalledStorePlugin) + : [], + sourceUrls: Array.isArray(value['sourceUrls']) + ? value['sourceUrls'] + .filter((entry): entry is string => typeof entry === 'string') + .map((entry) => normalizeOptionalSourceUrl(entry)) + .filter((entry): entry is string => !!entry) + : [] + }; +} + +function normalizePersistedServerPluginInstallState(value: unknown): { servers: Record } { + if (!isRecord(value) || !isRecord(value['servers'])) { + return { servers: {} }; + } + + const servers: Record = {}; + + for (const [serverId, installedPlugins] of Object.entries(value['servers'])) { + if (!Array.isArray(installedPlugins)) { + continue; + } + + const normalizedPlugins = installedPlugins + .filter(isInstalledStorePlugin) + .filter((installedPlugin) => getPluginInstallScope(installedPlugin.manifest) === 'server') + .sort(sortInstalledPlugins); + + if (normalizedPlugins.length > 0) { + servers[serverId] = normalizedPlugins; + } + } + + return { servers }; +} + +function isInstalledStorePlugin(value: unknown): value is InstalledStorePlugin { + if (!isRecord(value) || !isRecord(value['manifest'])) { + return false; + } + + const validation = validateTojuPluginManifest(value['manifest']); + + return !!validation.manifest; +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value); +} + +function readString(record: Record, ...keys: string[]): string | undefined { + for (const key of keys) { + const value = record[key]; + + if (typeof value === 'string' && value.trim()) { + return value.trim(); + } + } + + return undefined; +} + +function readAuthor(record: Record): string | undefined { + const author = readString(record, 'author'); + + if (author) { + return author; + } + + const authors = record['authors']; + + if (!Array.isArray(authors)) { + return undefined; + } + + return authors + .map((entry) => isRecord(entry) ? readString(entry, 'name') : typeof entry === 'string' ? entry.trim() : '') + .filter(Boolean) + .join(', ') || undefined; +} + +function readGithubUrl(record: Record): string | undefined { + const directUrl = readString(record, 'github', 'githubUrl'); + + if (directUrl) { + return directUrl; + } + + const repository = record['repository']; + + return isRecord(repository) ? readString(repository, 'url') : typeof repository === 'string' ? repository.trim() : undefined; +} + +function normalizeSourceUrl(rawUrl: string, label: string): string { + const url = normalizeOptionalSourceUrl(rawUrl); + + if (!url) { + throw new Error(`${label} must be an http, https, file URL, or absolute local path`); + } + + return url; +} + +function normalizeOptionalSourceUrl(rawUrl: string): string | undefined { + const trimmedUrl = rawUrl.trim(); + + if (!trimmedUrl) { + return undefined; + } + + try { + const url = new URL(trimmedUrl); + + if (!isAllowedPluginSourceProtocol(url.protocol)) { + return undefined; + } + + url.hash = ''; + return url.toString(); + } catch { + return localPathToFileUrl(trimmedUrl); + } +} + +function resolveOptionalUrl(sourceUrl: string, rawUrl?: string): string | undefined { + if (!rawUrl) { + return undefined; + } + + try { + const url = new URL(rawUrl, sourceUrl); + + if (!isAllowedPluginSourceProtocol(url.protocol)) { + return undefined; + } + + url.hash = ''; + return url.toString(); + } catch { + return undefined; + } +} + +function isAllowedPluginSourceProtocol(protocol: string): boolean { + return protocol === 'http:' || protocol === 'https:' || protocol === 'file:'; +} + +function isAbsolutePluginUrl(value: string): boolean { + try { + const url = new URL(value); + + return isAllowedPluginSourceProtocol(url.protocol); + } catch { + return false; + } +} + +function sanitizePathSegment(value: string): string { + return value.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 128) || 'plugin'; +} + +function joinLocalPath(...parts: string[]): string { + return parts + .map((part, index) => index === 0 ? part.replace(/[\\/]+$/, '') : part.replace(/^[\\/]+|[\\/]+$/g, '')) + .filter(Boolean) + .join('/'); +} + +function dirnameLocalPath(filePath: string): string { + const normalized = filePath.replace(/\\/g, '/'); + const index = normalized.lastIndexOf('/'); + + return index > 0 ? normalized.slice(0, index) : normalized; +} + +function bytesToBase64(bytes: Uint8Array): string { + let binary = ''; + + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + + return btoa(binary); +} + +function localPathToFileUrl(filePath: string): string | undefined { + if (!isAbsoluteLocalPath(filePath)) { + return undefined; + } + + const normalizedPath = filePath.replace(/\\/g, '/'); + const pathWithLeadingSlash = /^[A-Za-z]:\//.test(normalizedPath) + ? `/${normalizedPath}` + : normalizedPath; + + return `file://${pathWithLeadingSlash.split('/') + .map(encodeURIComponent) + .join('/')}`; +} + +function fileUrlToPath(fileUrl: string): string { + const url = new URL(fileUrl); + const decodedPath = decodeURIComponent(url.pathname); + + if (/^\/[A-Za-z]:\//.test(decodedPath)) { + return decodedPath.slice(1).replace(/\//g, '\\'); + } + + return decodedPath; +} + +function isAbsoluteLocalPath(filePath: string): boolean { + return filePath.startsWith('/') || /^[A-Za-z]:[\\/]/.test(filePath); +} + +function compareVersions(leftVersion: string, rightVersion: string): number { + const leftParts = parseVersion(leftVersion); + const rightParts = parseVersion(rightVersion); + + for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) { + const leftPart = leftParts[index] ?? 0; + const rightPart = rightParts[index] ?? 0; + + if (leftPart !== rightPart) { + return leftPart - rightPart; + } + } + + return leftVersion.localeCompare(rightVersion); +} + +function parseVersion(version: string): number[] { + return version + .split(/[.+-]/) + .slice(0, 3) + .map((part) => Number.parseInt(part, 10)) + .map((part) => Number.isFinite(part) ? part : 0); +} diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-ui-registry.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-ui-registry.service.ts new file mode 100644 index 0000000..9d678d1 --- /dev/null +++ b/toju-app/src/app/domains/plugins/application/services/plugin-ui-registry.service.ts @@ -0,0 +1,237 @@ +import { + Injectable, + Signal, + computed, + signal +} from '@angular/core'; +import type { + PluginApiActionContribution, + PluginApiChannelSectionContribution, + PluginApiDomMountRequest, + PluginApiEmbedRendererContribution, + PluginApiPageContribution, + PluginApiPanelContribution, + PluginApiSettingsPageContribution, + PluginApiUiContributionMap, + TojuPluginDisposable +} from '../../domain/models/plugin-api.models'; + +type ContributionKind = keyof PluginApiUiContributionMap; + +export interface PluginUiContributionRecord { + contribution: TContribution; + contributionKey: string; + id: string; + pluginId: string; +} + +export interface PluginUiConflictDiagnostic { + contributionId: string; + kind: ContributionKind; + pluginIds: string[]; +} + +interface PluginDomMountRecord { + element: HTMLElement; + id: string; + pluginId: string; +} + +@Injectable({ providedIn: 'root' }) +export class PluginUiRegistryService { + readonly appPages = this.createContributionSignal('appPages'); + readonly appPageRecords = this.createContributionRecordSignal('appPages'); + readonly channelSections = this.createContributionSignal('channelSections'); + readonly channelSectionRecords = this.createContributionRecordSignal('channelSections'); + readonly composerActions = this.createContributionSignal('composerActions'); + readonly composerActionRecords = this.createContributionRecordSignal('composerActions'); + readonly embeds = this.createContributionSignal('embeds'); + readonly embedRecords = this.createContributionRecordSignal('embeds'); + readonly profileActions = this.createContributionSignal('profileActions'); + readonly profileActionRecords = this.createContributionRecordSignal('profileActions'); + readonly settingsPages = this.createContributionSignal('settingsPages'); + readonly settingsPageRecords = this.createContributionRecordSignal('settingsPages'); + readonly sidePanels = this.createContributionSignal('sidePanels'); + readonly sidePanelRecords = this.createContributionRecordSignal('sidePanels'); + readonly toolbarActions = this.createContributionSignal('toolbarActions'); + readonly toolbarActionRecords = this.createContributionRecordSignal('toolbarActions'); + readonly conflicts = computed(() => this.collectConflicts()); + private readonly domMounts = new Map(); + + private readonly contributionsSignal = signal<{ + appPages: PluginUiContributionRecord[]; + channelSections: PluginUiContributionRecord[]; + composerActions: PluginUiContributionRecord[]; + embeds: PluginUiContributionRecord[]; + profileActions: PluginUiContributionRecord[]; + settingsPages: PluginUiContributionRecord[]; + sidePanels: PluginUiContributionRecord[]; + toolbarActions: PluginUiContributionRecord[]; + }>({ + appPages: [], + channelSections: [], + composerActions: [], + embeds: [], + profileActions: [], + settingsPages: [], + sidePanels: [], + toolbarActions: [] + }); + + registerAppPage(pluginId: string, id: string, contribution: PluginApiPageContribution): TojuPluginDisposable { + return this.register('appPages', pluginId, id, contribution); + } + + registerChannelSection(pluginId: string, id: string, contribution: PluginApiChannelSectionContribution): TojuPluginDisposable { + return this.register('channelSections', pluginId, id, contribution); + } + + registerComposerAction(pluginId: string, id: string, contribution: PluginApiActionContribution): TojuPluginDisposable { + return this.register('composerActions', pluginId, id, contribution); + } + + registerEmbedRenderer(pluginId: string, id: string, contribution: PluginApiEmbedRendererContribution): TojuPluginDisposable { + return this.register('embeds', pluginId, id, contribution); + } + + mountElement(pluginId: string, id: string, request: PluginApiDomMountRequest): TojuPluginDisposable { + const mountId = `${pluginId}:${id}`; + const target = this.resolveMountTarget(request.target); + + if (!target) { + throw new Error(`Plugin mount target not found: ${typeof request.target === 'string' ? request.target : request.target.tagName}`); + } + + this.unmountElement(mountId); + request.element.dataset['pluginOwner'] = pluginId; + request.element.dataset['pluginMountId'] = mountId; + target.insertAdjacentElement(request.position ?? 'beforeend', request.element); + this.domMounts.set(mountId, { element: request.element, id: mountId, pluginId }); + + return { + dispose: () => this.unmountElement(mountId) + }; + } + + registerProfileAction(pluginId: string, id: string, contribution: PluginApiActionContribution): TojuPluginDisposable { + return this.register('profileActions', pluginId, id, contribution); + } + + registerSettingsPage(pluginId: string, id: string, contribution: PluginApiSettingsPageContribution): TojuPluginDisposable { + return this.register('settingsPages', pluginId, id, contribution); + } + + registerSidePanel(pluginId: string, id: string, contribution: PluginApiPanelContribution): TojuPluginDisposable { + return this.register('sidePanels', pluginId, id, contribution); + } + + registerToolbarAction(pluginId: string, id: string, contribution: PluginApiActionContribution): TojuPluginDisposable { + return this.register('toolbarActions', pluginId, id, contribution); + } + + unregisterPlugin(pluginId: string): void { + for (const mount of this.domMounts.values()) { + if (mount.pluginId === pluginId) { + this.unmountElement(mount.id); + } + } + + this.contributionsSignal.update((current) => ({ + appPages: current.appPages.filter((entry) => entry.pluginId !== pluginId), + channelSections: current.channelSections.filter((entry) => entry.pluginId !== pluginId), + composerActions: current.composerActions.filter((entry) => entry.pluginId !== pluginId), + embeds: current.embeds.filter((entry) => entry.pluginId !== pluginId), + profileActions: current.profileActions.filter((entry) => entry.pluginId !== pluginId), + settingsPages: current.settingsPages.filter((entry) => entry.pluginId !== pluginId), + sidePanels: current.sidePanels.filter((entry) => entry.pluginId !== pluginId), + toolbarActions: current.toolbarActions.filter((entry) => entry.pluginId !== pluginId) + })); + } + + private register( + kind: TKind, + pluginId: string, + id: string, + contribution: PluginApiUiContributionMap[TKind][number] + ): TojuPluginDisposable { + const contributionId = `${pluginId}:${id}`; + + this.contributionsSignal.update((current) => ({ + ...current, + [kind]: [ + ...current[kind].filter((entry) => entry.id !== contributionId), + { + contribution, + contributionKey: id, + id: contributionId, + pluginId + } + ] + })); + + return { + dispose: () => this.unregister(kind, contributionId) + }; + } + + private unregister(kind: ContributionKind, contributionId: string): void { + this.contributionsSignal.update((current) => ({ + ...current, + [kind]: current[kind].filter((entry) => entry.id !== contributionId) + })); + } + + private resolveMountTarget(target: Element | string): Element | null { + return typeof target === 'string' + ? document.querySelector(target) + : target; + } + + private unmountElement(mountId: string): void { + const mount = this.domMounts.get(mountId); + + if (!mount) { + return; + } + + mount.element.remove(); + this.domMounts.delete(mountId); + } + + private createContributionSignal(kind: TKind): Signal { + return computed(() => this.contributionsSignal()[kind].map((entry) => entry.contribution) as PluginApiUiContributionMap[TKind]); + } + + private createContributionRecordSignal( + kind: TKind + ): Signal[]> { + return computed(() => this.contributionsSignal()[kind] as PluginUiContributionRecord[]); + } + + private collectConflicts(): PluginUiConflictDiagnostic[] { + const conflicts: PluginUiConflictDiagnostic[] = []; + + for (const kind of Object.keys(this.contributionsSignal()) as ContributionKind[]) { + const byKey = new Map>(); + + for (const entry of this.contributionsSignal()[kind]) { + const pluginIds = byKey.get(entry.contributionKey) ?? new Set(); + + pluginIds.add(entry.pluginId); + byKey.set(entry.contributionKey, pluginIds); + } + + for (const [contributionId, pluginIds] of byKey.entries()) { + if (pluginIds.size > 1) { + conflicts.push({ + contributionId, + kind, + pluginIds: Array.from(pluginIds).sort() + }); + } + } + } + + return conflicts; + } +} diff --git a/toju-app/src/app/domains/plugins/development/development-plugin.ts b/toju-app/src/app/domains/plugins/development/development-plugin.ts new file mode 100644 index 0000000..84019eb --- /dev/null +++ b/toju-app/src/app/domains/plugins/development/development-plugin.ts @@ -0,0 +1,43 @@ +import type { TojuPluginManifest } from '../../../shared-kernel'; +import type { TojuClientPluginModule } from '../domain/models/plugin-api.models'; + +export const DEVELOPMENT_PLUGIN_ENTRYPOINT = 'toju:development-plugin'; + +export const DEVELOPMENT_PLUGIN_MANIFEST: TojuPluginManifest = { + apiVersion: '1.0.0', + capabilities: [], + compatibility: { + minimumTojuVersion: '1.0.0', + verifiedTojuVersion: '1.0.0' + }, + description: 'Built-in development-only plugin for validating the local plugin runtime.', + entrypoint: DEVELOPMENT_PLUGIN_ENTRYPOINT, + homepage: 'https://localhost:4200', + id: 'metoyou.development-plugin', + kind: 'client', + readme: 'Only registered when the Angular app is running with environment.production=false.', + schemaVersion: 1, + settings: { + properties: { + enabled: { + default: true, + type: 'boolean' + } + }, + type: 'object' + }, + title: 'Development Plugin', + version: '0.0.0-dev' +}; + +export const DEVELOPMENT_PLUGIN_MODULE: TojuClientPluginModule = { + activate: (context) => { + context.api.logger.info('Development plugin activated'); + }, + deactivate: (context) => { + context.api.logger.info('Development plugin deactivated'); + }, + ready: (context) => { + context.api.logger.info('Development plugin ready'); + } +}; diff --git a/toju-app/src/app/domains/plugins/domain/logic/plugin-dependency-resolver.logic.spec.ts b/toju-app/src/app/domains/plugins/domain/logic/plugin-dependency-resolver.logic.spec.ts new file mode 100644 index 0000000..94c10f3 --- /dev/null +++ b/toju-app/src/app/domains/plugins/domain/logic/plugin-dependency-resolver.logic.spec.ts @@ -0,0 +1,82 @@ +import type { TojuPluginManifest } from '../../../../shared-kernel'; +import { resolvePluginLoadOrder } from './plugin-dependency-resolver.logic'; + +function manifest(id: string, overrides: Partial = {}): TojuPluginManifest { + return { + apiVersion: '1.0.0', + compatibility: { + minimumTojuVersion: '1.0.0' + }, + description: `${id} plugin`, + entrypoint: './main.js', + id, + kind: 'client', + schemaVersion: 1, + title: id, + version: '1.0.0', + ...overrides + }; +} + +describe('plugin dependency resolver', () => { + it('orders required dependencies before dependants', () => { + const featurePlugin = manifest('feature.chat', { relationships: { requires: [{ id: 'library.base' }] } }); + const result = resolvePluginLoadOrder([{ manifest: featurePlugin }, { manifest: manifest('library.base') }]); + + expect(result.blocked).toEqual([]); + expect(result.ordered.map((entry) => entry.id)).toEqual(['library.base', 'feature.chat']); + }); + + it('uses priority then plugin id for otherwise independent plugins', () => { + const result = resolvePluginLoadOrder([ + { manifest: manifest('plugin.zed') }, + { manifest: manifest('plugin.bootstrap', { load: { priority: 'bootstrap' } }) }, + { manifest: manifest('plugin.alpha') } + ]); + + expect(result.ordered.map((entry) => entry.id)).toEqual([ + 'plugin.bootstrap', + 'plugin.alpha', + 'plugin.zed' + ]); + }); + + it('blocks missing dependencies and leaves valid plugins loadable', () => { + const blockedPlugin = manifest('plugin.blocked', { relationships: { requires: [{ id: 'missing.library' }] } }); + const result = resolvePluginLoadOrder([{ manifest: manifest('plugin.valid') }, { manifest: blockedPlugin }]); + + expect(result.ordered.map((entry) => entry.id)).toEqual(['plugin.valid']); + expect(result.blocked).toContainEqual({ + message: 'Missing required plugin missing.library', + pluginId: 'plugin.blocked', + reason: 'missingDependency' + }); + }); + + it('detects duplicate ids and cycles', () => { + const result = resolvePluginLoadOrder([ + { manifest: manifest('plugin.duplicate') }, + { manifest: manifest('plugin.duplicate') }, + { manifest: manifest('plugin.a', { relationships: { after: ['plugin.b'] } }) }, + { manifest: manifest('plugin.b', { relationships: { after: ['plugin.a'] } }) } + ]); + + expect(result.blocked).toEqual(expect.arrayContaining([ + { + message: 'Duplicate plugin id', + pluginId: 'plugin.duplicate', + reason: 'duplicate' + }, + { + message: 'Plugin load order contains a cycle', + pluginId: 'plugin.a', + reason: 'cycle' + }, + { + message: 'Plugin load order contains a cycle', + pluginId: 'plugin.b', + reason: 'cycle' + } + ])); + }); +}); diff --git a/toju-app/src/app/domains/plugins/domain/logic/plugin-dependency-resolver.logic.ts b/toju-app/src/app/domains/plugins/domain/logic/plugin-dependency-resolver.logic.ts new file mode 100644 index 0000000..ce00bcb --- /dev/null +++ b/toju-app/src/app/domains/plugins/domain/logic/plugin-dependency-resolver.logic.ts @@ -0,0 +1,251 @@ +import type { TojuPluginManifest } from '../../../../shared-kernel'; +import type { + PluginLoadBlocker, + PluginLoadCandidate, + PluginLoadOrderResult +} from '../models/plugin-runtime.models'; + +const PRIORITY_WEIGHT: Record = { + bootstrap: 0, + high: 1, + default: 2, + low: 3 +}; + +interface PluginLoadGraph { + edges: Map>; + inboundCounts: Map; +} + +function priorityWeight(manifest: TojuPluginManifest): number { + return PRIORITY_WEIGHT[manifest.load?.priority ?? 'default'] ?? PRIORITY_WEIGHT['default']; +} + +function sortManifests(firstManifest: TojuPluginManifest, secondManifest: TojuPluginManifest): number { + const firstPriority = priorityWeight(firstManifest); + const secondPriority = priorityWeight(secondManifest); + + if (firstPriority !== secondPriority) { + return firstPriority - secondPriority; + } + + return firstManifest.id.localeCompare(secondManifest.id); +} + +function addEdge(edges: Map>, fromPluginId: string, toPluginId: string): void { + const targets = edges.get(fromPluginId) ?? new Set(); + + targets.add(toPluginId); + edges.set(fromPluginId, targets); +} + +function addBlocker(blocked: PluginLoadBlocker[], pluginId: string, reason: PluginLoadBlocker['reason'], message: string): void { + blocked.push({ pluginId, reason, message }); +} + +function collectManifests( + candidates: readonly PluginLoadCandidate[], + blocked: PluginLoadBlocker[] +): Map { + const manifestsById = new Map(); + + for (const candidate of candidates) { + if (candidate.enabled === false) { + addBlocker(blocked, candidate.manifest.id, 'disabled', 'Plugin is disabled'); + continue; + } + + if (manifestsById.has(candidate.manifest.id)) { + addBlocker(blocked, candidate.manifest.id, 'duplicate', 'Duplicate plugin id'); + continue; + } + + manifestsById.set(candidate.manifest.id, candidate.manifest); + } + + return manifestsById; +} + +function createLoadGraph(manifestsById: Map): PluginLoadGraph { + const graph: PluginLoadGraph = { + edges: new Map>(), + inboundCounts: new Map() + }; + + for (const pluginId of manifestsById.keys()) { + graph.edges.set(pluginId, new Set()); + graph.inboundCounts.set(pluginId, 0); + } + + return graph; +} + +function addRequiredEdges( + manifest: TojuPluginManifest, + manifestsById: Map, + edges: Map>, + blocked: PluginLoadBlocker[] +): void { + for (const required of manifest.relationships?.requires ?? []) { + if (!manifestsById.has(required.id)) { + addBlocker(blocked, manifest.id, 'missingDependency', `Missing required plugin ${required.id}`); + continue; + } + + addEdge(edges, required.id, manifest.id); + } +} + +function addOrderingEdges( + manifest: TojuPluginManifest, + manifestsById: Map, + edges: Map> +): void { + for (const afterPluginId of manifest.relationships?.after ?? []) { + if (manifestsById.has(afterPluginId)) { + addEdge(edges, afterPluginId, manifest.id); + } + } + + for (const beforePluginId of manifest.relationships?.before ?? []) { + if (manifestsById.has(beforePluginId)) { + addEdge(edges, manifest.id, beforePluginId); + } + } +} + +function addConflictBlockers( + manifest: TojuPluginManifest, + manifestsById: Map, + blocked: PluginLoadBlocker[] +): void { + for (const conflictPluginId of manifest.relationships?.conflicts ?? []) { + if (manifestsById.has(conflictPluginId)) { + addBlocker(blocked, manifest.id, 'conflict', `Conflicts with plugin ${conflictPluginId}`); + } + } +} + +function applyRelationships( + manifestsById: Map, + edges: Map>, + blocked: PluginLoadBlocker[] +): void { + for (const manifest of manifestsById.values()) { + addRequiredEdges(manifest, manifestsById, edges, blocked); + addOrderingEdges(manifest, manifestsById, edges); + addConflictBlockers(manifest, manifestsById, blocked); + } +} + +function countInboundEdges(graph: PluginLoadGraph, blockedIds: Set): void { + for (const [fromPluginId, targets] of graph.edges.entries()) { + if (blockedIds.has(fromPluginId)) { + continue; + } + + for (const targetPluginId of targets) { + if (!blockedIds.has(targetPluginId)) { + graph.inboundCounts.set(targetPluginId, (graph.inboundCounts.get(targetPluginId) ?? 0) + 1); + } + } + } +} + +function getInitialReadyManifests( + manifestsById: Map, + inboundCounts: Map, + blockedIds: Set +): TojuPluginManifest[] { + return Array.from(manifestsById.values()) + .filter((manifest) => !blockedIds.has(manifest.id) && (inboundCounts.get(manifest.id) ?? 0) === 0) + .sort(sortManifests); +} + +function pushReadyManifest( + ready: TojuPluginManifest[], + manifestsById: Map, + pluginId: string +): void { + const targetManifest = manifestsById.get(pluginId); + + if (targetManifest) { + ready.push(targetManifest); + ready.sort(sortManifests); + } +} + +function consumeReadyManifest( + manifest: TojuPluginManifest, + graph: PluginLoadGraph, + manifestsById: Map, + ready: TojuPluginManifest[], + blockedIds: Set +): void { + for (const targetPluginId of graph.edges.get(manifest.id) ?? []) { + if (blockedIds.has(targetPluginId)) { + continue; + } + + const nextInboundCount = Math.max(0, (graph.inboundCounts.get(targetPluginId) ?? 0) - 1); + + graph.inboundCounts.set(targetPluginId, nextInboundCount); + + if (nextInboundCount === 0) { + pushReadyManifest(ready, manifestsById, targetPluginId); + } + } +} + +function buildOrderedManifests( + graph: PluginLoadGraph, + manifestsById: Map, + blockedIds: Set +): TojuPluginManifest[] { + const ready = getInitialReadyManifests(manifestsById, graph.inboundCounts, blockedIds); + const ordered: TojuPluginManifest[] = []; + + while (ready.length > 0) { + const nextManifest = ready.shift(); + + if (!nextManifest) { + break; + } + + ordered.push(nextManifest); + consumeReadyManifest(nextManifest, graph, manifestsById, ready, blockedIds); + } + + return ordered; +} + +function addCycleBlockers( + manifestsById: Map, + ordered: TojuPluginManifest[], + blockedIds: Set, + blocked: PluginLoadBlocker[] +): void { + const orderedIds = new Set(ordered.map((manifest) => manifest.id)); + + for (const manifest of manifestsById.values()) { + if (!blockedIds.has(manifest.id) && !orderedIds.has(manifest.id)) { + addBlocker(blocked, manifest.id, 'cycle', 'Plugin load order contains a cycle'); + } + } +} + +export function resolvePluginLoadOrder(candidates: readonly PluginLoadCandidate[]): PluginLoadOrderResult { + const blocked: PluginLoadBlocker[] = []; + const manifestsById = collectManifests(candidates, blocked); + const graph = createLoadGraph(manifestsById); + + applyRelationships(manifestsById, graph.edges, blocked); + const blockedIds = new Set(blocked.map((blocker) => blocker.pluginId)); + + countInboundEdges(graph, blockedIds); + const ordered = buildOrderedManifests(graph, manifestsById, blockedIds); + + addCycleBlockers(manifestsById, ordered, blockedIds, blocked); + + return { blocked, ordered }; +} diff --git a/toju-app/src/app/domains/plugins/domain/logic/plugin-install-scope.logic.ts b/toju-app/src/app/domains/plugins/domain/logic/plugin-install-scope.logic.ts new file mode 100644 index 0000000..ac3149f --- /dev/null +++ b/toju-app/src/app/domains/plugins/domain/logic/plugin-install-scope.logic.ts @@ -0,0 +1,5 @@ +import type { TojuPluginInstallScope, TojuPluginManifest } from '../../../../shared-kernel'; + +export function getPluginInstallScope(manifest: Pick): TojuPluginInstallScope { + return manifest.scope === 'server' ? 'server' : 'client'; +} diff --git a/toju-app/src/app/domains/plugins/domain/logic/plugin-manifest-validation.logic.spec.ts b/toju-app/src/app/domains/plugins/domain/logic/plugin-manifest-validation.logic.spec.ts new file mode 100644 index 0000000..5c2d2c5 --- /dev/null +++ b/toju-app/src/app/domains/plugins/domain/logic/plugin-manifest-validation.logic.spec.ts @@ -0,0 +1,109 @@ +import type { TojuPluginManifest } from '../../../../shared-kernel'; +import { isKnownPluginCapability, validateTojuPluginManifest } from './plugin-manifest-validation.logic'; + +function createManifest(overrides: Partial = {}): TojuPluginManifest { + return { + apiVersion: '1.0.0', + compatibility: { + minimumTojuVersion: '1.0.0' + }, + description: 'Adds test behavior.', + entrypoint: './main.js', + id: 'test.plugin', + kind: 'client', + schemaVersion: 1, + title: 'Test Plugin', + version: '1.2.3', + ...overrides + }; +} + +describe('plugin manifest validation', () => { + it('accepts a valid client plugin manifest', () => { + const result = validateTojuPluginManifest(createManifest({ + capabilities: ['messages.send', 'ui.settings'], + events: [ + { + direction: 'serverRelay', + eventName: 'test:ping', + scope: 'server' + } + ] + })); + + expect(result.valid).toBe(true); + expect(result.manifest?.id).toBe('test.plugin'); + expect(result.issues).toEqual([]); + }); + + it('rejects executable client manifests without an entrypoint', () => { + const manifest = createManifest({ entrypoint: undefined }); + const result = validateTojuPluginManifest(manifest); + + expect(result.valid).toBe(false); + expect(result.manifest).toBeUndefined(); + expect(result.issues).toContainEqual({ + message: 'client plugins require an entrypoint', + path: 'entrypoint', + severity: 'error' + }); + }); + + it('allows library manifests without an entrypoint', () => { + const result = validateTojuPluginManifest(createManifest({ + entrypoint: undefined, + kind: 'library' + })); + + expect(result.valid).toBe(true); + }); + + it('accepts server-scoped client plugin manifests', () => { + const result = validateTojuPluginManifest(createManifest({ + scope: 'server' + })); + + expect(result.valid).toBe(true); + expect(result.manifest?.scope).toBe('server'); + }); + + it('rejects unknown plugin install scopes', () => { + const result = validateTojuPluginManifest({ + ...createManifest(), + scope: 'workspace' + }); + + expect(result.valid).toBe(false); + expect(result.issues).toContainEqual({ + message: 'scope must be client or server', + path: 'scope', + severity: 'error' + }); + }); + + it('rejects unknown capabilities and event dimensions', () => { + const result = validateTojuPluginManifest({ + ...createManifest(), + capabilities: ['messages.send', 'unknown.power'], + events: [ + { + direction: 'serverMagic', + eventName: 'bad-event', + scope: 'cosmos' + } + ] + }); + + expect(result.valid).toBe(false); + expect(result.issues.map((issue) => issue.path)).toEqual(expect.arrayContaining([ + 'capabilities.1', + 'events.0.direction', + 'events.0.scope' + ])); + }); + + it('narrows known plugin capabilities', () => { + expect(isKnownPluginCapability('messages.send')).toBe(true); + expect(isKnownPluginCapability('messages.destroyEverything')).toBe(false); + }); +}); diff --git a/toju-app/src/app/domains/plugins/domain/logic/plugin-manifest-validation.logic.ts b/toju-app/src/app/domains/plugins/domain/logic/plugin-manifest-validation.logic.ts new file mode 100644 index 0000000..1ca2475 --- /dev/null +++ b/toju-app/src/app/domains/plugins/domain/logic/plugin-manifest-validation.logic.ts @@ -0,0 +1,208 @@ +import { + PLUGIN_CAPABILITIES, + PLUGIN_EVENT_DIRECTIONS, + PLUGIN_EVENT_SCOPES, + type PluginCapabilityId, + type TojuPluginManifest +} from '../../../../shared-kernel'; +import type { PluginManifestValidationResult, PluginValidationIssue } from '../models/plugin-runtime.models'; + +const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]{1,126}[a-z0-9]$/; +const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; +const capabilitySet = new Set(PLUGIN_CAPABILITIES); +const eventDirectionSet = new Set(PLUGIN_EVENT_DIRECTIONS); +const eventScopeSet = new Set(PLUGIN_EVENT_SCOPES); + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value); +} + +function readString(record: Record, key: string): string | null { + const value = record[key]; + + return typeof value === 'string' ? value.trim() : null; +} + +function pushIssue( + issues: PluginValidationIssue[], + path: string, + message: string, + severity: PluginValidationIssue['severity'] = 'error' +): void { + issues.push({ path, message, severity }); +} + +function validateStringField( + issues: PluginValidationIssue[], + record: Record, + key: string, + options?: { pattern?: RegExp; required?: boolean } +): void { + const value = readString(record, key); + + if (!value) { + if (options?.required !== false) { + pushIssue(issues, key, `${key} is required`); + } + + return; + } + + if (options?.pattern && !options.pattern.test(value)) { + pushIssue(issues, key, `${key} has an invalid format`); + } +} + +function validateStringArray( + issues: PluginValidationIssue[], + value: unknown, + path: string +): void { + if (value === undefined) { + return; + } + + if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || !entry.trim())) { + pushIssue(issues, path, `${path} must be an array of non-empty strings`); + } +} + +function validateRelationships(issues: PluginValidationIssue[], manifestRecord: Record): void { + const relationships = manifestRecord['relationships']; + + if (relationships === undefined) { + return; + } + + if (!isRecord(relationships)) { + pushIssue(issues, 'relationships', 'relationships must be an object'); + return; + } + + validateStringArray(issues, relationships['after'], 'relationships.after'); + validateStringArray(issues, relationships['before'], 'relationships.before'); + validateStringArray(issues, relationships['conflicts'], 'relationships.conflicts'); + + for (const key of ['requires', 'optional'] as const) { + const entries = relationships[key]; + + if (entries === undefined) { + continue; + } + + if (!Array.isArray(entries)) { + pushIssue(issues, `relationships.${key}`, `relationships.${key} must be an array`); + continue; + } + + entries.forEach((entry, index) => { + if (!isRecord(entry) || typeof entry['id'] !== 'string' || !entry['id'].trim()) { + pushIssue(issues, `relationships.${key}.${index}`, 'dependency id is required'); + } + }); + } +} + +function validateCapabilities(issues: PluginValidationIssue[], manifestRecord: Record): void { + const capabilities = manifestRecord['capabilities']; + + if (capabilities === undefined) { + return; + } + + if (!Array.isArray(capabilities)) { + pushIssue(issues, 'capabilities', 'capabilities must be an array'); + return; + } + + capabilities.forEach((capability, index) => { + if (typeof capability !== 'string' || !capabilitySet.has(capability)) { + pushIssue(issues, `capabilities.${index}`, `Unknown capability ${String(capability)}`); + } + }); +} + +function validateEvents(issues: PluginValidationIssue[], manifestRecord: Record): void { + const events = manifestRecord['events']; + + if (events === undefined) { + return; + } + + if (!Array.isArray(events)) { + pushIssue(issues, 'events', 'events must be an array'); + return; + } + + events.forEach((event, index) => { + if (!isRecord(event)) { + pushIssue(issues, `events.${index}`, 'event must be an object'); + return; + } + + if (typeof event['eventName'] !== 'string' || !event['eventName'].trim()) { + pushIssue(issues, `events.${index}.eventName`, 'eventName is required'); + } + + if (typeof event['direction'] !== 'string' || !eventDirectionSet.has(event['direction'])) { + pushIssue(issues, `events.${index}.direction`, 'direction is invalid'); + } + + if (typeof event['scope'] !== 'string' || !eventScopeSet.has(event['scope'])) { + pushIssue(issues, `events.${index}.scope`, 'scope is invalid'); + } + }); +} + +export function validateTojuPluginManifest(value: unknown): PluginManifestValidationResult { + const issues: PluginValidationIssue[] = []; + + if (!isRecord(value)) { + return { + issues: [{ path: '', message: 'Manifest must be an object', severity: 'error' }], + valid: false + }; + } + + validateStringField(issues, value, 'id', { pattern: PLUGIN_ID_PATTERN }); + validateStringField(issues, value, 'title'); + validateStringField(issues, value, 'description'); + validateStringField(issues, value, 'version', { pattern: VERSION_PATTERN }); + validateStringField(issues, value, 'apiVersion'); + + if (value['schemaVersion'] !== 1) { + pushIssue(issues, 'schemaVersion', 'schemaVersion must be 1'); + } + + if (value['kind'] !== 'client' && value['kind'] !== 'library') { + pushIssue(issues, 'kind', 'kind must be client or library'); + } + + if (value['scope'] !== undefined && value['scope'] !== 'client' && value['scope'] !== 'server') { + pushIssue(issues, 'scope', 'scope must be client or server'); + } + + if (!isRecord(value['compatibility'])) { + pushIssue(issues, 'compatibility', 'compatibility is required'); + } else { + validateStringField(issues, value['compatibility'], 'minimumTojuVersion'); + } + + if (typeof value['entrypoint'] !== 'string' && value['kind'] === 'client') { + pushIssue(issues, 'entrypoint', 'client plugins require an entrypoint'); + } + + validateCapabilities(issues, value); + validateRelationships(issues, value); + validateEvents(issues, value); + + return { + issues, + manifest: issues.some((issue) => issue.severity === 'error') ? undefined : value as unknown as TojuPluginManifest, + valid: !issues.some((issue) => issue.severity === 'error') + }; +} + +export function isKnownPluginCapability(value: string): value is PluginCapabilityId { + return capabilitySet.has(value); +} diff --git a/toju-app/src/app/domains/plugins/domain/models/plugin-api.models.ts b/toju-app/src/app/domains/plugins/domain/models/plugin-api.models.ts new file mode 100644 index 0000000..3782cc0 --- /dev/null +++ b/toju-app/src/app/domains/plugins/domain/models/plugin-api.models.ts @@ -0,0 +1,296 @@ +import type { + Channel, + Message, + PluginEventEnvelope, + PluginRequirementsSnapshot, + Room, + RoomMember, + RoomPermissions, + RoomRole, + RoomRoleAssignment, + TojuPluginManifest, + User +} from '../../../../shared-kernel'; + +export interface TojuPluginDisposable { + dispose: () => void; +} + +export interface TojuPluginActivationContext { + api: TojuClientPluginApi; + manifest: TojuPluginManifest; + pluginId: string; + subscriptions: TojuPluginDisposable[]; +} + +export interface TojuClientPluginModule { + activate?: (context: TojuPluginActivationContext) => Promise | void; + deactivate?: (context: TojuPluginActivationContext) => Promise | void; + onPluginDataChanged?: (context: TojuPluginActivationContext, event: unknown) => Promise | void; + onServerRequirementsChanged?: (context: TojuPluginActivationContext, snapshot: PluginRequirementsSnapshot) => Promise | void; + ready?: (context: TojuPluginActivationContext) => Promise | void; +} + +export interface PluginApiProfileUpdate { + description?: string; + displayName: string; +} + +export interface PluginApiAvatarUpdate { + avatarHash: string; + avatarMime: string; + avatarUrl: string; +} + +export interface PluginApiChannelRequest { + id?: string; + name: string; + position?: number; +} + +export interface PluginApiServerSettingsUpdate { + description?: string; + isPrivate?: boolean; + maxUsers?: number; + name?: string; + password?: string; + topic?: string; +} + +export interface PluginApiPluginUserRequest { + avatarUrl?: string; + displayName: string; + id?: string; +} + +export interface PluginApiMessageAsPluginUserRequest { + channelId?: string; + content: string; + pluginUserId: string; +} + +export interface PluginApiAudioClipRequest { + volume?: number; + url: string; +} + +export interface PluginApiCustomStreamRequest { + label?: string; + stream: MediaStream; +} + +export interface PluginApiEventSubscription { + eventName: string; + handler: (event: PluginEventEnvelope) => void; +} + +export type PluginApiActionSource = 'composerAction' | 'toolbarAction' | 'profileAction' | 'manual'; + +export interface PluginApiActionContext { + server: Room | null; + source: PluginApiActionSource; + textChannel: Channel | null; + user: User | null; + voiceChannel: Channel | null; +} + +export interface PluginApiTypingEvent extends Omit { + channelId: string; + displayName: string; + isTyping: boolean; + serverId: string; + userId: string; +} + +export interface PluginApiMessageBusEnvelope { + channelId?: string; + eventId: string; + messages?: Message[]; + payload?: unknown; + pluginId: string; + roomId: string; + sentAt: number; + sourcePeerId?: string; + sourceUserId?: string; + topic: string; +} + +export interface PluginApiMessageBusLatestRequest { + channelId?: string; + includeDeleted?: boolean; + limit?: number; + sinceTimestamp?: number; + targetPeerId?: string; + topic?: string; +} + +export interface PluginApiMessageBusPublishRequest extends PluginApiMessageBusLatestRequest { + includeLatestMessages?: boolean; + includeSelf?: boolean; + payload?: unknown; + topic: string; +} + +export interface PluginApiMessageBusSubscription { + channelId?: string; + handler: (event: PluginApiMessageBusEnvelope) => void; + latestMessageLimit?: number; + replayLatest?: boolean; + topic?: string; +} + +export interface PluginApiSettingsPageContribution { + label: string; + order?: number; + render: () => HTMLElement | string; + settingsKey?: string; +} + +export interface PluginApiPageContribution { + label: string; + path: string; + render: () => HTMLElement | string; +} + +export interface PluginApiPanelContribution { + label: string; + order?: number; + render: () => HTMLElement | string; +} + +export interface PluginApiChannelSectionContribution { + label: string; + order?: number; + type?: 'audio' | 'custom' | 'video'; +} + +export interface PluginApiActionContribution { + icon?: string; + label: string; + run: (context: PluginApiActionContext) => Promise | void; +} + +export interface PluginApiEmbedRendererContribution { + embedType: string; + render: (payload: unknown) => HTMLElement | string; +} + +export interface PluginApiDomMountRequest { + element: HTMLElement; + position?: InsertPosition; + target: Element | string; +} + +export interface PluginApiUiContributionMap { + appPages: PluginApiPageContribution[]; + channelSections: PluginApiChannelSectionContribution[]; + composerActions: PluginApiActionContribution[]; + embeds: PluginApiEmbedRendererContribution[]; + profileActions: PluginApiActionContribution[]; + settingsPages: PluginApiSettingsPageContribution[]; + sidePanels: PluginApiPanelContribution[]; + toolbarActions: PluginApiActionContribution[]; +} + +export interface TojuClientPluginApi { + readonly channels: { + addAudioChannel: (request: PluginApiChannelRequest) => void; + addVideoChannel: (request: PluginApiChannelRequest) => void; + list: () => Channel[]; + remove: (channelId: string) => void; + rename: (channelId: string, name: string) => void; + select: (channelId: string) => void; + }; + readonly events: { + publishP2p: (eventName: string, payload: unknown) => void; + publishServer: (eventName: string, payload: unknown) => void; + subscribeP2p: (subscription: PluginApiEventSubscription) => TojuPluginDisposable; + subscribeServer: (subscription: PluginApiEventSubscription) => TojuPluginDisposable; + }; + readonly logger: { + debug: (message: string, data?: unknown) => void; + error: (message: string, data?: unknown) => void; + info: (message: string, data?: unknown) => void; + warn: (message: string, data?: unknown) => void; + }; + readonly context: { + getCurrent: () => PluginApiActionContext; + }; + readonly clientData: { + read: (key: string) => Promise; + remove: (key: string) => Promise; + write: (key: string, value: unknown) => Promise; + }; + readonly media: { + addCustomAudioStream: (request: PluginApiCustomStreamRequest) => Promise; + addCustomVideoStream: (request: PluginApiCustomStreamRequest) => Promise; + playAudioClip: (request: PluginApiAudioClipRequest) => Promise; + setInputVolume: (volume: number) => void; + setOutputVolume: (volume: number) => void; + }; + readonly messages: { + delete: (messageId: string) => void; + edit: (messageId: string, content: string) => void; + moderateDelete: (messageId: string) => void; + readCurrent: () => Message[]; + send: (content: string, channelId?: string) => Message; + sendAsPluginUser: (request: PluginApiMessageAsPluginUserRequest) => void; + setTyping: (isTyping: boolean, channelId?: string) => void; + subscribeTyping: (handler: (event: PluginApiTypingEvent) => void) => TojuPluginDisposable; + sync: (messages: Message[]) => void; + }; + readonly messageBus: { + publish: (request: PluginApiMessageBusPublishRequest) => PluginApiMessageBusEnvelope; + sendLatestMessages: (request?: PluginApiMessageBusLatestRequest) => PluginApiMessageBusEnvelope; + subscribe: (subscription: PluginApiMessageBusSubscription) => TojuPluginDisposable; + }; + readonly p2p: { + broadcastData: (eventName: string, payload: unknown) => void; + connectedPeers: () => string[]; + sendData: (peerId: string, eventName: string, payload: unknown) => void; + }; + readonly profile: { + getCurrent: () => User | null; + update: (profile: PluginApiProfileUpdate) => void; + updateAvatar: (avatar: PluginApiAvatarUpdate) => void; + }; + readonly roles: { + list: () => RoomRole[]; + setAssignments: (assignments: RoomRoleAssignment[]) => void; + }; + readonly server: { + getCurrent: () => Room | null; + registerPluginUser: (request: PluginApiPluginUserRequest) => string; + updatePermissions: (permissions: Partial) => void; + updateSettings: (settings: PluginApiServerSettingsUpdate) => void; + }; + readonly serverData: { + read: (key: string) => Promise; + remove: (key: string) => Promise; + write: (key: string, value: unknown) => Promise; + }; + readonly storage: { + get: (key: string) => unknown; + remove: (key: string) => void; + set: (key: string, value: unknown) => void; + }; + readonly ui: { + registerAppPage: (id: string, contribution: PluginApiPageContribution) => TojuPluginDisposable; + registerChannelSection: (id: string, contribution: PluginApiChannelSectionContribution) => TojuPluginDisposable; + registerComposerAction: (id: string, contribution: PluginApiActionContribution) => TojuPluginDisposable; + registerEmbedRenderer: (id: string, contribution: PluginApiEmbedRendererContribution) => TojuPluginDisposable; + mountElement: (id: string, request: PluginApiDomMountRequest) => TojuPluginDisposable; + registerProfileAction: (id: string, contribution: PluginApiActionContribution) => TojuPluginDisposable; + registerSettingsPage: (id: string, contribution: PluginApiSettingsPageContribution) => TojuPluginDisposable; + registerSidePanel: (id: string, contribution: PluginApiPanelContribution) => TojuPluginDisposable; + registerToolbarAction: (id: string, contribution: PluginApiActionContribution) => TojuPluginDisposable; + }; + readonly users: { + ban: (userId: string, reason?: string) => void; + getCurrent: () => User | null; + kick: (userId: string) => void; + list: () => User[]; + readMembers: () => RoomMember[]; + setRole: (userId: string, role: User['role']) => void; + }; +} diff --git a/toju-app/src/app/domains/plugins/domain/models/plugin-runtime.models.ts b/toju-app/src/app/domains/plugins/domain/models/plugin-runtime.models.ts new file mode 100644 index 0000000..5a45947 --- /dev/null +++ b/toju-app/src/app/domains/plugins/domain/models/plugin-runtime.models.ts @@ -0,0 +1,81 @@ +import type { TojuPluginManifest } from '../../../../shared-kernel'; + +export type PluginRuntimeState = + | 'discovered' + | 'validated' + | 'blocked' + | 'loading' + | 'ready' + | 'loaded' + | 'failed' + | 'unloading' + | 'unloaded' + | 'disabled'; + +export type PluginValidationSeverity = 'error' | 'warning'; + +export interface PluginValidationIssue { + message: string; + path: string; + severity: PluginValidationSeverity; +} + +export interface PluginManifestValidationResult { + issues: PluginValidationIssue[]; + manifest?: TojuPluginManifest; + valid: boolean; +} + +export interface RegisteredPlugin { + enabled: boolean; + error?: string; + loadIndex?: number; + manifest: TojuPluginManifest; + sourcePath?: string; + state: PluginRuntimeState; + validationIssues: PluginValidationIssue[]; +} + +export interface PluginLoadCandidate { + enabled?: boolean; + manifest: TojuPluginManifest; +} + +export interface PluginLoadBlocker { + message: string; + pluginId: string; + reason: 'conflict' | 'cycle' | 'disabled' | 'duplicate' | 'missingDependency' | 'validation'; +} + +export interface PluginLoadOrderResult { + blocked: PluginLoadBlocker[]; + ordered: TojuPluginManifest[]; +} + +export interface LocalPluginManifestDescriptor { + discoveredAt: number; + entrypointPath?: string; + pluginRootUrl?: string; + manifest: unknown; + manifestPath: string; + pluginRoot: string; + readmePath?: string; +} + +export interface LocalPluginDiscoveryError { + manifestPath?: string; + message: string; + pluginRoot?: string; +} + +export interface LocalPluginDiscoveryResult { + errors: LocalPluginDiscoveryError[]; + plugins: LocalPluginManifestDescriptor[]; + pluginsPath: string; +} + +export interface LocalPluginRegistrationResult { + discovery: LocalPluginDiscoveryResult; + errors: LocalPluginDiscoveryError[]; + registered: RegisteredPlugin[]; +} diff --git a/toju-app/src/app/domains/plugins/domain/models/plugin-store.models.ts b/toju-app/src/app/domains/plugins/domain/models/plugin-store.models.ts new file mode 100644 index 0000000..3edadb1 --- /dev/null +++ b/toju-app/src/app/domains/plugins/domain/models/plugin-store.models.ts @@ -0,0 +1,57 @@ +import type { TojuPluginInstallScope, TojuPluginManifest } from '../../../../shared-kernel'; + +export type PluginStoreInstallState = 'installed' | 'notInstalled' | 'updateAvailable'; +export type PluginStoreActionLabel = 'Install' | 'Install to Server' | 'Remove from Server' | 'Uninstall' | 'Update' | 'Update Server'; + +export interface PluginStoreEntry { + author?: string; + bundleUrl?: string; + description: string; + githubUrl?: string; + homepageUrl?: string; + id: string; + imageUrl?: string; + installUrl?: string; + readmeUrl?: string; + scope?: TojuPluginInstallScope; + sourceTitle?: string; + sourceUrl: string; + title: string; + version: string; +} + +export interface PluginStoreSourceResult { + error?: string; + loadedAt?: number; + plugins: PluginStoreEntry[]; + title?: string; + url: string; +} + +export interface InstalledStorePlugin { + bundleUrl?: string; + cachedAt?: number; + cachedSourcePath?: string; + installedAt: number; + installUrl?: string; + manifest: TojuPluginManifest; + sourceUrl?: string; + updatedAt: number; +} + +export interface PluginStoreReadme { + pluginId: string; + title: string; + url: string; + markdown: string; +} + +export interface PersistedPluginStoreState { + installedPlugins: InstalledStorePlugin[]; + sourceUrls: string[]; +} + +export interface PersistedServerPluginInstallState { + schemaVersion?: number; + servers?: Record; +} diff --git a/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.html b/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.html new file mode 100644 index 0000000..3812e33 --- /dev/null +++ b/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.html @@ -0,0 +1,461 @@ + +
+
+
+ +
+

{{ managerTitle() }}

+

{{ managerDescription() }}

+
+
+ + +
+ + + +
+ @switch (activeTab()) { + @case ('extensions') { +
+
+ @for ( + item of [ + { label: 'Settings pages', value: extensionCounts().settingsPages }, + { label: 'App pages', value: extensionCounts().appPages }, + { label: 'Side panels', value: extensionCounts().sidePanels }, + { label: 'Channel sections', value: extensionCounts().channelSections }, + { label: 'Composer actions', value: extensionCounts().composerActions }, + { label: 'Profile actions', value: extensionCounts().profileActions }, + { label: 'Toolbar actions', value: extensionCounts().toolbarActions }, + { label: 'Embed renderers', value: extensionCounts().embeds } + ]; + track item.label + ) { +
+

{{ item.label }}

+

{{ item.value }}

+
+ } +
+ +
+

Conflict diagnostics

+ @if (uiConflicts().length === 0) { +

+ No duplicate route, action, embed, channel, panel, or settings contribution ids detected. +

+ } @else { +
+ @for (conflict of uiConflicts(); track conflict.kind + conflict.contributionId) { +
+ {{ conflict.kind }} / {{ conflict.contributionId }} + conflicts in {{ conflict.pluginIds.join(', ') }} +
+ } +
+ } +
+
+ } + @case ('requirements') { +
+ @if (requirementComparisons().length === 0) { +

+ No server plugin requirements for the current room. +

+ } @else { + @for (comparison of requirementComparisons(); track comparison.pluginId) { +
+
+
+

{{ comparison.installed?.title ?? comparison.pluginId }}

+

{{ comparison.pluginId }}

+
+ {{ comparison.status }} +
+ @if (comparison.requirement) { +

Server status: {{ comparison.requirement.status }}

+ @if (comparison.requirement.versionRange) { +

Version range: {{ comparison.requirement.versionRange }}

+ } + @if (comparison.requirement.reason) { +

{{ comparison.requirement.reason }}

+ } + } +
+ } + } +
+ } + @case ('settings') { +
+
+ @for (entry of entries(); track trackEntry($index, entry)) { + + } +
+
+ @if (selectedPlugin(); as plugin) { +

{{ plugin.manifest.title }} settings

+ @if (selectedSettingsPages().length > 0) { +
+ @for (page of selectedSettingsPages(); track page.id) { +
+

{{ page.contribution.label }}

+ +
+ } +
+ } + @if (selectedSettingsSchema()) { +
{{ selectedSettingsSchema() | json }}
+ } @else { +

This plugin does not declare a settings schema.

+ } + } +
+
+ } + @case ('docs') { +
+
+ @for (entry of entries(); track trackEntry($index, entry)) { + + } +
+
+ @if (selectedPlugin(); as plugin) { +

{{ plugin.manifest.title }}

+

{{ plugin.manifest.description }}

+
+ @for (doc of selectedDocs(); track doc.label) { + {{ doc.label }} + } +
+
{{ plugin.manifest | json }}
+ } +
+
+ } + @case ('logs') { +
+ @if (!selectedPlugin()) { +

No plugins installed.

+ } @else { +
+ @for (entry of entries(); track trackEntry($index, entry)) { + + } +
+
+ @if (selectedLogs().length === 0) { +

No logs for selected plugin.

+ } @else { + @for (log of selectedLogs(); track log.timestamp) { +
+
+ {{ log.level }} + {{ log.timestamp | date: 'short' }} +
+

{{ log.message }}

+
+ } + } +
+ } +
+ } + @default { +
+
+ @if (entries().length === 0) { +
+ +

{{ emptyTitle() }}

+

{{ emptyBody() }}

+
+ } @else { + @for (entry of entries(); track trackEntry($index, entry)) { +
+
+
+
+

{{ entry.manifest.title }}

+ {{ entry.state }} + v{{ entry.manifest.version }} +
+

{{ entry.manifest.description }}

+

{{ entry.manifest.id }}

+
+
+ + + + + +
+
+ @if (entry.error) { +

{{ entry.error }}

+ } +
+ } + } +
+ + +
+ } + } +
+
diff --git a/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.ts b/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.ts new file mode 100644 index 0000000..80eb064 --- /dev/null +++ b/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.ts @@ -0,0 +1,245 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + EventEmitter, + Output, + computed, + inject, + input, + signal +} from '@angular/core'; +import { Router } from '@angular/router'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { + lucideArrowLeft, + lucideBug, + lucideCheck, + lucidePackage, + lucidePlay, + lucideRefreshCw, + lucideSettings, + lucideShield, + lucideStore, + lucideX +} from '@ng-icons/lucide'; +import type { PluginCapabilityId, TojuPluginInstallScope } from '../../../../shared-kernel'; +import { PluginCapabilityService } from '../../application/services/plugin-capability.service'; +import { PluginHostService } from '../../application/services/plugin-host.service'; +import { PluginLoggerService } from '../../application/services/plugin-logger.service'; +import { PluginRegistryService } from '../../application/services/plugin-registry.service'; +import { PluginRequirementStateService } from '../../application/services/plugin-requirement-state.service'; +import { PluginUiRegistryService } from '../../application/services/plugin-ui-registry.service'; +import { getPluginInstallScope } from '../../domain/logic/plugin-install-scope.logic'; +import type { RegisteredPlugin } from '../../domain/models/plugin-runtime.models'; +import { PluginRenderHostComponent } from '../plugin-render-host/plugin-render-host.component'; + +type PluginManagerTab = 'docs' | 'extensions' | 'installed' | 'logs' | 'requirements' | 'settings'; + +@Component({ + selector: 'app-plugin-manager', + standalone: true, + imports: [ + CommonModule, + NgIcon, + PluginRenderHostComponent + ], + templateUrl: './plugin-manager.component.html', + viewProviders: [ + provideIcons({ + lucideArrowLeft, + lucideBug, + lucideCheck, + lucidePackage, + lucidePlay, + lucideRefreshCw, + lucideSettings, + lucideShield, + lucideStore, + lucideX + }) + ] +}) +export class PluginManagerComponent { + @Output() readonly closed = new EventEmitter(); + @Output() readonly storeOpened = new EventEmitter(); + + readonly scope = input('client'); + + readonly capabilities = inject(PluginCapabilityService); + readonly host = inject(PluginHostService); + readonly logger = inject(PluginLoggerService); + readonly registry = inject(PluginRegistryService); + readonly requirementState = inject(PluginRequirementStateService); + readonly router = inject(Router); + readonly uiRegistry = inject(PluginUiRegistryService); + readonly activeTab = signal('installed'); + readonly busyPluginId = signal(null); + readonly busyAll = signal(false); + readonly selectedPluginId = signal(null); + readonly allEntries = this.registry.entries; + readonly entries = computed(() => this.allEntries().filter((entry) => this.entryBelongsToScope(entry))); + readonly managerTitle = computed(() => this.scope() === 'server' ? 'Server plugins' : 'Client plugins'); + readonly managerDescription = computed(() => this.scope() === 'server' + ? 'Plugins installed for the current chat server.' + : 'Global client plugins installed on this device.'); + readonly selectedPlugin = computed(() => { + const selectedPluginId = this.selectedPluginId(); + + return this.entries().find((entry) => entry.manifest.id === selectedPluginId) ?? this.entries()[0] ?? null; + }); + readonly missingCapabilities = computed(() => { + const selectedPlugin = this.selectedPlugin(); + + return selectedPlugin ? this.capabilities.missing(selectedPlugin.manifest) : []; + }); + readonly selectedLogs = computed(() => { + const selectedPlugin = this.selectedPlugin(); + + return selectedPlugin ? this.logger.entries().filter((entry) => entry.pluginId === selectedPlugin.manifest.id) + .slice(-20) : []; + }); + readonly extensionCounts = computed(() => ({ + appPages: this.uiRegistry.appPageRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length, + channelSections: this.uiRegistry.channelSectionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length, + composerActions: this.uiRegistry.composerActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length, + embeds: this.uiRegistry.embedRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length, + profileActions: this.uiRegistry.profileActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length, + settingsPages: this.uiRegistry.settingsPageRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length, + sidePanels: this.uiRegistry.sidePanelRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length, + toolbarActions: this.uiRegistry.toolbarActionRecords().filter((record) => this.hasVisiblePlugin(record.pluginId)).length + })); + readonly requirementComparisons = computed(() => this.scope() === 'server' ? this.requirementState.comparisons() : []); + readonly uiConflicts = computed(() => this.uiRegistry.conflicts() + .filter((conflict) => conflict.pluginIds.some((pluginId) => this.hasVisiblePlugin(pluginId)))); + readonly selectedRequirement = computed(() => { + const selectedPlugin = this.selectedPlugin(); + + return selectedPlugin ? this.requirementState.comparisonFor(selectedPlugin.manifest.id) : null; + }); + readonly selectedSettingsSchema = computed(() => this.selectedPlugin()?.manifest.settings ?? null); + readonly selectedSettingsPages = computed(() => { + const selectedPlugin = this.selectedPlugin(); + + return selectedPlugin + ? this.uiRegistry.settingsPageRecords().filter((record) => record.pluginId === selectedPlugin.manifest.id) + : []; + }); + readonly emptyTitle = computed(() => this.scope() === 'server' ? 'No server plugins installed.' : 'No client plugins installed.'); + readonly emptyBody = computed(() => this.scope() === 'server' + ? 'Server-scoped plugins use scope: server in toju-plugin.json.' + : 'Client-scoped plugins use scope: client or omit scope in toju-plugin.json.'); + readonly selectedDocs = computed(() => { + const manifest = this.selectedPlugin()?.manifest; + + if (!manifest) { + return []; + } + + return [ + { label: 'Readme', url: manifest.readme }, + { label: 'Homepage', url: manifest.homepage }, + { label: 'Changelog', url: manifest.changelog }, + { label: 'Support', url: manifest.bugs } + ].filter((item): item is { label: string; url: string } => typeof item.url === 'string' && item.url.length > 0); + }); + + setTab(tab: PluginManagerTab): void { + this.activeTab.set(tab); + } + + openStore(): void { + const returnUrl = this.router.url.startsWith('/plugin-store') ? '/search' : this.router.url; + + this.storeOpened.emit(); + void this.router.navigate(['/plugin-store'], { queryParams: { returnUrl } }); + } + + selectPlugin(pluginId: string): void { + this.selectedPluginId.set(pluginId); + } + + grantAll(entry: RegisteredPlugin): void { + this.capabilities.grantAll(entry.manifest); + } + + toggleCapability(entry: RegisteredPlugin, capability: PluginCapabilityId): void { + if (this.capabilities.has(entry.manifest.id, capability)) { + this.capabilities.revoke(entry.manifest.id, capability); + return; + } + + this.capabilities.grant(entry.manifest.id, capability); + } + + async activateAll(): Promise { + this.busyAll.set(true); + + try { + await this.host.activateReadyPlugins(); + } finally { + this.busyAll.set(false); + } + } + + async reload(entry: RegisteredPlugin): Promise { + this.busyPluginId.set(entry.manifest.id); + + try { + await this.host.reloadPlugin(entry.manifest.id); + } finally { + this.busyPluginId.set(null); + } + } + + async activate(entry: RegisteredPlugin): Promise { + this.busyPluginId.set(entry.manifest.id); + + try { + await this.host.activatePluginById(entry.manifest.id); + } finally { + this.busyPluginId.set(null); + } + } + + async unload(entry: RegisteredPlugin): Promise { + this.busyPluginId.set(entry.manifest.id); + + try { + await this.host.deactivatePlugin(entry.manifest.id, { forgetActivation: true }); + } finally { + this.busyPluginId.set(null); + } + } + + setEnabled(entry: RegisteredPlugin, enabled: boolean): void { + this.registry.setEnabled(entry.manifest.id, enabled); + } + + isSelected(entry: RegisteredPlugin): boolean { + return this.selectedPlugin()?.manifest.id === entry.manifest.id; + } + + isActive(entry: RegisteredPlugin): boolean { + return this.host.isPluginActive(entry.manifest.id); + } + + close(): void { + this.closed.emit(); + } + + trackEntry(index: number, entry: RegisteredPlugin): string { + return entry.manifest.id; + } + + trackCapability(index: number, capability: PluginCapabilityId): string { + return capability; + } + + private entryBelongsToScope(entry: RegisteredPlugin): boolean { + return getPluginInstallScope(entry.manifest) === this.scope(); + } + + private hasVisiblePlugin(pluginId: string): boolean { + return this.entries().some((entry) => entry.manifest.id === pluginId); + } +} diff --git a/toju-app/src/app/domains/plugins/feature/plugin-page-host/plugin-page-host.component.html b/toju-app/src/app/domains/plugins/feature/plugin-page-host/plugin-page-host.component.html new file mode 100644 index 0000000..a9853d7 --- /dev/null +++ b/toju-app/src/app/domains/plugins/feature/plugin-page-host/plugin-page-host.component.html @@ -0,0 +1,21 @@ +
+ Back + @if (page(); as pageRecord) { +
+

{{ pageRecord.pluginId }}

+

{{ pageRecord.contribution.label }}

+
+ +
+
+ } @else { +
+

Plugin page unavailable

+

The plugin page is not registered or the plugin is not loaded.

+
+ } +
diff --git a/toju-app/src/app/domains/plugins/feature/plugin-page-host/plugin-page-host.component.ts b/toju-app/src/app/domains/plugins/feature/plugin-page-host/plugin-page-host.component.ts new file mode 100644 index 0000000..29ffff4 --- /dev/null +++ b/toju-app/src/app/domains/plugins/feature/plugin-page-host/plugin-page-host.component.ts @@ -0,0 +1,42 @@ +import { toSignal } from '@angular/core/rxjs-interop'; +import { CommonModule } from '@angular/common'; +import { + Component, + computed, + inject +} from '@angular/core'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { map } from 'rxjs/operators'; +import { PluginUiRegistryService } from '../../application/services/plugin-ui-registry.service'; +import { PluginRenderHostComponent } from '../plugin-render-host/plugin-render-host.component'; + +@Component({ + selector: 'app-plugin-page-host', + standalone: true, + imports: [ + CommonModule, + RouterLink, + PluginRenderHostComponent + ], + templateUrl: './plugin-page-host.component.html' +}) +export class PluginPageHostComponent { + readonly page = computed(() => { + const params = this.params(); + + if (!params?.pluginId || !params.pageId) { + return null; + } + + return this.uiRegistry.appPageRecords().find((record) => + record.pluginId === params.pluginId && record.contributionKey === params.pageId + ) ?? null; + }); + + private readonly route = inject(ActivatedRoute); + private readonly uiRegistry = inject(PluginUiRegistryService); + private readonly params = toSignal(this.route.paramMap.pipe(map((params) => ({ + pageId: params.get('pageId'), + pluginId: params.get('pluginId') + })))); +} diff --git a/toju-app/src/app/domains/plugins/feature/plugin-render-host/plugin-render-host.component.ts b/toju-app/src/app/domains/plugins/feature/plugin-render-host/plugin-render-host.component.ts new file mode 100644 index 0000000..0d90b05 --- /dev/null +++ b/toju-app/src/app/domains/plugins/feature/plugin-render-host/plugin-render-host.component.ts @@ -0,0 +1,44 @@ +import { + Component, + ElementRef, + effect, + input, + viewChild +} from '@angular/core'; + +export type PluginRenderable = () => HTMLElement | string; + +@Component({ + selector: 'app-plugin-render-host', + standalone: true, + template: '
' +}) +export class PluginRenderHostComponent { + readonly render = input.required(); + private readonly host = viewChild.required>('host'); + + constructor() { + effect(() => { + this.renderContribution(this.render()); + }); + } + + private renderContribution(render: PluginRenderable): void { + const hostElement = this.host().nativeElement; + + hostElement.replaceChildren(); + + try { + const rendered = render(); + + if (typeof rendered === 'string') { + hostElement.textContent = rendered; + return; + } + + hostElement.appendChild(rendered); + } catch (error) { + hostElement.textContent = error instanceof Error ? error.message : 'Plugin contribution failed to render'; + } + } +} diff --git a/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.html b/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.html new file mode 100644 index 0000000..3da3da2 --- /dev/null +++ b/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.html @@ -0,0 +1,544 @@ + +
+
+
+ + +
+ +
+ +
+

Plugin Store

+

+ {{ installedCount() }} installed for {{ store.installScopeLabel() }} · {{ totalSourcePlugins() }} available · {{ sourceCount() }} sources +

+
+
+ +
+ + +
+
+ +
+
+ + +
+ + @if (sourceError()) { +

{{ sourceError() }}

+ } +
+ +
+ + +
+
+ + +
{{ filteredPlugins().length }} shown
+
+ + @if (actionError()) { +

{{ actionError() }}

+ } + + @if (readmeError()) { +

{{ readmeError() }}

+ } + + @if (filteredPlugins().length > 0) { +
+ @for (plugin of filteredPlugins(); track trackPlugin($index, plugin)) { +
+
+ @if (plugin.imageUrl) { + + } @else { + + } +
+ +
+
+
+

{{ plugin.title }}

+

{{ plugin.author || 'Unknown author' }} · v{{ plugin.version }}

+
+ + @if (getPluginInstallState(plugin) === 'updateAvailable') { + Update + } + @if (getPluginInstallState(plugin) === 'installed') { + Installed + } +
+ +

{{ plugin.description }}

+ +
+ {{ plugin.id }} + {{ plugin.sourceTitle || plugin.sourceUrl }} +
+ +
+ + + @if (plugin.readmeUrl) { + + } + + @if (plugin.githubUrl) { + + } +
+
+
+ } +
+ } @else { +
+
+ +

No plugins found

+

+ {{ sourceCount() ? 'Adjust filters or add another source manifest.' : 'Add a plugin source manifest URL to populate the catalog.' }} +

+
+
+ } +
+ + @if (readme()) { + + } +
+ + @if (serverInstallDialog(); as dialog) { + + + } +
diff --git a/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.ts b/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.ts new file mode 100644 index 0000000..afdf246 --- /dev/null +++ b/toju-app/src/app/domains/plugins/feature/plugin-store/plugin-store.component.ts @@ -0,0 +1,623 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + DestroyRef, + OnInit, + computed, + effect, + inject, + signal +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Store as NgRxStore } from '@ngrx/store'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { + lucideArrowLeft, + lucideExternalLink, + lucidePlus, + lucidePackage, + lucideRefreshCw, + lucideSearch, + lucideSettings, + lucideStore, + lucideTrash2, + lucideX +} from '@ng-icons/lucide'; +import { ExternalLinkService } from '../../../../core/platform'; +import { SettingsModalService } from '../../../../core/services/settings-modal.service'; +import { ChatMessageMarkdownComponent } from '../../../chat'; +import { resolveLegacyRole, resolveRoomPermission } from '../../../access-control'; +import type { + PluginCapabilityId, + Room, + TojuPluginManifest, + User +} from '../../../../shared-kernel'; +import { selectCurrentRoom, selectSavedRooms } from '../../../../store/rooms/rooms.selectors'; +import { selectCurrentUser } from '../../../../store/users/users.selectors'; +import { PluginCapabilityService } from '../../application/services/plugin-capability.service'; +import { PluginStoreService } from '../../application/services/plugin-store.service'; +import type { + InstalledStorePlugin, + PluginStoreEntry, + PluginStoreInstallState, + PluginStoreReadme +} from '../../domain/models/plugin-store.models'; + +interface ServerPluginInstallDialog { + manifest: TojuPluginManifest; + plugin: PluginStoreEntry; + selectedServerId: string; +} + +@Component({ + selector: 'app-plugin-store', + standalone: true, + imports: [ + CommonModule, + FormsModule, + ChatMessageMarkdownComponent, + NgIcon + ], + viewProviders: [ + provideIcons({ + lucideArrowLeft, + lucideExternalLink, + lucidePlus, + lucidePackage, + lucideRefreshCw, + lucideSearch, + lucideSettings, + lucideStore, + lucideTrash2, + lucideX + }) + ], + templateUrl: './plugin-store.component.html' +}) +export class PluginStoreComponent implements OnInit { + readonly store = inject(PluginStoreService); + readonly capabilities = inject(PluginCapabilityService); + readonly ngrxStore = inject(NgRxStore); + readonly savedRooms = this.ngrxStore.selectSignal(selectSavedRooms); + readonly currentRoom = this.ngrxStore.selectSignal(selectCurrentRoom); + readonly currentUser = this.ngrxStore.selectSignal(selectCurrentUser); + readonly manageableServers = computed(() => { + const user = this.currentUser(); + + if (!user) { + return []; + } + + const roomsById = new Map(this.savedRooms().map((room) => [room.id, room])); + const currentRoom = this.currentRoom(); + + if (currentRoom) { + roomsById.set(currentRoom.id, currentRoom); + } + + return Array.from(roomsById.values()) + .filter((room) => this.canManageServerPlugins(room, user)); + }); + readonly sourceErrors = computed(() => this.store.sources().filter((source) => !!source.error)); + readonly installedIds = computed(() => new Set(this.store.installedPlugins().map((plugin) => plugin.manifest.id))); + readonly filteredPlugins = computed(() => { + const searchTerm = this.searchTerm().trim() + .toLowerCase(); + const sourceFilter = this.selectedSourceUrl(); + const showInstalled = this.showInstalledOnly(); + const installedIds = this.installedIds(); + const plugins = this.store.availablePlugins() + .filter((plugin) => !sourceFilter || plugin.sourceUrl === sourceFilter) + .filter((plugin) => !showInstalled || installedIds.has(plugin.id)); + + if (!searchTerm) { + return plugins; + } + + return plugins.filter((plugin) => this.matchesSearch(plugin, searchTerm)); + }); + readonly installedCount = computed(() => this.store.installedPlugins().length); + readonly totalSourcePlugins = computed(() => this.store.availablePlugins().length); + readonly sourceCount = computed(() => this.store.sourceUrls().length); + readonly pendingSourceUrls = computed(() => { + const loadedUrls = new Set(this.store.sources().map((source) => source.url)); + + return this.store.sourceUrls().filter((sourceUrl) => !loadedUrls.has(sourceUrl)); + }); + readonly selectedReadmePlugin = computed(() => { + const readme = this.readme(); + + return readme ? this.store.availablePlugins().find((plugin) => plugin.id === readme.pluginId) ?? null : null; + }); + readonly selectedStoreServer = computed(() => { + const selectedServerId = this.selectedStoreServerId(); + + return selectedServerId ? this.manageableServers().find((server) => server.id === selectedServerId) ?? null : null; + }); + + newSourceUrl = ''; + readonly searchTerm = signal(''); + readonly selectedSourceUrl = signal(null); + readonly selectedStoreServerId = signal(null); + readonly selectedServerInstalledPlugins = signal([]); + readonly showInstalledOnly = signal(false); + readonly sourceError = signal(null); + readonly actionError = signal(null); + readonly actionBusyPluginId = signal(null); + readonly readme = signal(null); + readonly readmeRawMode = signal(false); + readonly readmeError = signal(null); + readonly readmeLoadingPluginId = signal(null); + readonly serverInstallDialog = signal(null); + readonly selectedCapabilityIds = signal>(new Set()); + readonly serverInstallOptional = signal(false); + readonly serverInstallError = signal(null); + readonly serverInstallBusy = signal(false); + + private destroyed = false; + private readonly destroyRef = inject(DestroyRef); + private readonly externalLinks = inject(ExternalLinkService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly settingsModal = inject(SettingsModalService); + private selectedServerLoadVersion = 0; + + constructor() { + effect(() => { + const servers = this.manageableServers(); + const selectedServerId = this.selectedStoreServerId(); + + if (servers.length === 0) { + this.selectedStoreServerId.set(null); + this.selectedServerInstalledPlugins.set([]); + return; + } + + if (!selectedServerId || !servers.some((server) => server.id === selectedServerId)) { + this.selectedStoreServerId.set(servers[0].id); + return; + } + + void this.loadSelectedServerInstalledPlugins(selectedServerId); + }); + + this.destroyRef.onDestroy(() => { + this.destroyed = true; + }); + } + + ngOnInit(): void { + const searchQuery = this.route.snapshot.queryParamMap.get('search')?.trim(); + + if (searchQuery) { + this.searchTerm.set(searchQuery); + } + + if (this.store.sourceUrls().length > 0 && this.store.sources().length === 0) { + void this.refreshSources(); + } + } + + async addSourceUrl(): Promise { + const sourceUrl = this.newSourceUrl.trim(); + + if (!sourceUrl) { + return; + } + + this.sourceError.set(null); + + try { + await this.store.addSourceUrl(sourceUrl); + + if (this.destroyed) { + return; + } + + this.newSourceUrl = ''; + } catch (error) { + if (this.destroyed) { + return; + } + + this.sourceError.set(error instanceof Error ? error.message : 'Unable to add plugin source'); + } + } + + async removeSourceUrl(sourceUrl: string): Promise { + this.sourceError.set(null); + + try { + await this.store.removeSourceUrl(sourceUrl); + + if (this.selectedSourceUrl() === sourceUrl) { + this.selectedSourceUrl.set(null); + } + } catch (error) { + if (this.destroyed) { + return; + } + + this.sourceError.set(error instanceof Error ? error.message : 'Unable to remove plugin source'); + } + } + + async refreshSources(): Promise { + this.sourceError.set(null); + + try { + await this.store.refreshSources(); + } catch (error) { + if (this.destroyed) { + return; + } + + this.sourceError.set(error instanceof Error ? error.message : 'Unable to refresh plugin sources'); + } + } + + async runPrimaryAction(plugin: PluginStoreEntry): Promise { + const action = this.getPrimaryActionLabel(plugin); + + this.actionError.set(null); + this.actionBusyPluginId.set(plugin.id); + + try { + if (action === 'Uninstall') { + await this.store.uninstallPlugin(plugin.id, plugin.scope); + } else if (action === 'Remove from Server') { + await this.store.uninstallPlugin(plugin.id, plugin.scope, { serverId: this.selectedStoreServerId() ?? undefined }); + await this.refreshSelectedServerInstalledPlugins(); + } else if (this.isServerScopedPlugin(plugin)) { + await this.openServerInstallDialog(plugin); + } else { + await this.store.installPlugin(plugin); + } + } catch (error) { + if (this.destroyed) { + return; + } + + this.actionError.set(error instanceof Error ? error.message : 'Unable to update plugin installation'); + } finally { + if (!this.destroyed) { + this.actionBusyPluginId.set(null); + } + } + } + + async loadReadme(plugin: PluginStoreEntry): Promise { + this.readmeError.set(null); + this.readmeLoadingPluginId.set(plugin.id); + + try { + const readme = await this.store.loadReadme(plugin); + + if (this.destroyed) { + return; + } + + this.readme.set(readme); + this.readmeRawMode.set(false); + } catch (error) { + if (this.destroyed) { + return; + } + + this.readmeError.set(error instanceof Error ? error.message : 'Unable to load readme'); + } finally { + if (!this.destroyed) { + this.readmeLoadingPluginId.set(null); + } + } + } + + closeReadme(): void { + this.readme.set(null); + this.readmeRawMode.set(false); + this.readmeError.set(null); + } + + toggleReadmeRawMode(): void { + this.readmeRawMode.update((value) => !value); + } + + async openServerInstallDialog(plugin: PluginStoreEntry): Promise { + this.actionBusyPluginId.set(plugin.id); + this.serverInstallError.set(null); + + try { + const manifest = await this.store.loadInstallManifest(plugin); + const selectedServerId = this.selectedStoreServerId(); + + if (!selectedServerId) { + throw new Error('You need owner or Manage Server access on a chat server before installing server plugins'); + } + + this.selectedCapabilityIds.set(new Set(manifest.capabilities ?? [])); + this.serverInstallOptional.set(false); + this.serverInstallDialog.set({ manifest, plugin, selectedServerId }); + } catch (error) { + if (this.destroyed) { + return; + } + + this.actionError.set(error instanceof Error ? error.message : 'Unable to prepare server plugin install'); + } finally { + if (!this.destroyed) { + this.actionBusyPluginId.set(null); + } + } + } + + closeServerInstallDialog(): void { + if (this.serverInstallBusy()) { + return; + } + + this.serverInstallDialog.set(null); + this.serverInstallError.set(null); + this.serverInstallOptional.set(false); + this.selectedCapabilityIds.set(new Set()); + } + + selectServerInstallTarget(serverId: string): void { + this.selectedStoreServerId.set(serverId); + this.serverInstallDialog.update((dialog) => dialog ? { ...dialog, selectedServerId: serverId } : dialog); + } + + selectStoreServer(serverId: string): void { + this.selectedStoreServerId.set(serverId); + } + + toggleInstallCapability(capability: PluginCapabilityId, checked: boolean): void { + this.selectedCapabilityIds.update((capabilities) => { + const nextCapabilities = new Set(capabilities); + + if (checked) { + nextCapabilities.add(capability); + } else { + nextCapabilities.delete(capability); + } + + return nextCapabilities; + }); + } + + async confirmServerInstall(): Promise { + const dialog = this.serverInstallDialog(); + + if (!dialog) { + return; + } + + this.serverInstallBusy.set(true); + this.serverInstallError.set(null); + + try { + for (const capability of dialog.manifest.capabilities ?? []) { + if (this.selectedCapabilityIds().has(capability)) { + this.capabilities.grant(dialog.manifest.id, capability); + } else { + this.capabilities.revoke(dialog.manifest.id, capability); + } + } + + await this.store.installPlugin(dialog.plugin, { + activate: true, + manifest: dialog.manifest, + optional: this.serverInstallOptional(), + serverId: dialog.selectedServerId + }); + + await this.loadSelectedServerInstalledPlugins(dialog.selectedServerId); + + if (this.destroyed) { + return; + } + + this.serverInstallDialog.set(null); + this.serverInstallOptional.set(false); + this.selectedCapabilityIds.set(new Set()); + } catch (error) { + if (this.destroyed) { + return; + } + + this.serverInstallError.set(error instanceof Error ? error.message : 'Unable to install server plugin'); + } finally { + if (!this.destroyed) { + this.serverInstallBusy.set(false); + } + } + } + + goBack(): void { + void this.router.navigateByUrl(this.getReturnUrl()); + } + + async openManager(): Promise { + const currentRoomId = this.currentRoom()?.id; + + await this.router.navigateByUrl(this.getReturnUrl()); + this.settingsModal.open(this.store.hasActiveServerInstallScope() ? 'serverPlugins' : 'plugins', currentRoomId); + } + + selectSource(sourceUrl: string | null): void { + this.selectedSourceUrl.set(sourceUrl); + } + + toggleInstalledOnly(): void { + this.showInstalledOnly.update((value) => !value); + } + + openExternal(url?: string): void { + if (url) { + this.externalLinks.open(url); + } + } + + isPluginBusy(plugin: PluginStoreEntry): boolean { + return this.actionBusyPluginId() === plugin.id; + } + + isReadmeLoading(plugin: PluginStoreEntry): boolean { + return this.readmeLoadingPluginId() === plugin.id; + } + + isPrimaryActionDisabled(plugin: PluginStoreEntry): boolean { + return this.isPluginBusy(plugin) + || !this.canRunPrimaryAction(plugin) + || (!plugin.installUrl && this.getPluginInstallState(plugin) !== 'installed'); + } + + canRunPrimaryAction(plugin: PluginStoreEntry): boolean { + if (!this.isServerScopedPlugin(plugin)) { + return this.store.canInstallPlugin(plugin); + } + + return this.manageableServers().length > 0; + } + + getPluginInstallState(plugin: PluginStoreEntry): PluginStoreInstallState { + if (!this.isServerScopedPlugin(plugin)) { + return this.store.getInstallState(plugin); + } + + const installedPlugin = this.selectedServerInstalledPlugins() + .find((candidate) => candidate.manifest.id === plugin.id); + + if (!installedPlugin) { + return 'notInstalled'; + } + + return comparePluginVersions(plugin.version, installedPlugin.manifest.version) > 0 + ? 'updateAvailable' + : 'installed'; + } + + getPrimaryActionLabel(plugin: PluginStoreEntry): string { + if (!this.isServerScopedPlugin(plugin)) { + return this.store.getActionLabel(plugin); + } + + const state = this.getPluginInstallState(plugin); + + if (state === 'updateAvailable') { + return 'Update Server'; + } + + return state === 'installed' ? 'Remove from Server' : 'Install to Server'; + } + + primaryActionIcon(plugin: PluginStoreEntry): string { + const action = this.getPrimaryActionLabel(plugin); + + if (action === 'Uninstall') { + return 'lucideTrash2'; + } + + if (action === 'Remove from Server') { + return 'lucideTrash2'; + } + + return 'lucidePlus'; + } + + trackPlugin(index: number, plugin: PluginStoreEntry): string { + return `${plugin.sourceUrl}:${plugin.id}`; + } + + hideBrokenImage(event: Event): void { + const image = event.target as HTMLImageElement | null; + + if (image) { + image.hidden = true; + } + } + + trackServer(index: number, server: Room): string { + return server.id; + } + + trackInstallCapability(index: number, capability: PluginCapabilityId): string { + return capability; + } + + isServerScopedPlugin(plugin: PluginStoreEntry): boolean { + return plugin.scope === 'server'; + } + + serverInstallButtonTitle(plugin: PluginStoreEntry): string { + return this.isServerScopedPlugin(plugin) && this.manageableServers().length === 0 + ? 'Requires owner or Manage Server access on a chat server' + : this.getPrimaryActionLabel(plugin); + } + + private matchesSearch(plugin: PluginStoreEntry, searchTerm: string): boolean { + return [ + plugin.author, + plugin.description, + plugin.id, + plugin.sourceTitle, + plugin.title, + plugin.version + ].some((value) => value?.toLowerCase().includes(searchTerm)); + } + + private getReturnUrl(): string { + const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl'); + + if (returnUrl?.startsWith('/') && !returnUrl.startsWith('//') && !returnUrl.startsWith('/plugin-store')) { + return returnUrl; + } + + return '/search'; + } + + private canManageServerPlugins(room: Room, user: User): boolean { + return resolveLegacyRole(room, user) === 'host' || resolveRoomPermission(room, user, 'manageServer'); + } + + private async refreshSelectedServerInstalledPlugins(): Promise { + const selectedServerId = this.selectedStoreServerId(); + + if (selectedServerId) { + await this.loadSelectedServerInstalledPlugins(selectedServerId); + } + } + + private async loadSelectedServerInstalledPlugins(serverId: string): Promise { + const loadVersion = ++this.selectedServerLoadVersion; + + try { + const installedPlugins = await this.store.loadInstalledPluginsForServer(serverId); + + if (!this.destroyed && loadVersion === this.selectedServerLoadVersion && this.selectedStoreServerId() === serverId) { + this.selectedServerInstalledPlugins.set(installedPlugins); + } + } catch { + if (!this.destroyed && loadVersion === this.selectedServerLoadVersion && this.selectedStoreServerId() === serverId) { + this.selectedServerInstalledPlugins.set([]); + } + } + } +} + +function comparePluginVersions(leftVersion: string, rightVersion: string): number { + const leftParts = leftVersion.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0); + const rightParts = rightVersion.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0); + const length = Math.max(leftParts.length, rightParts.length); + + for (let index = 0; index < length; index += 1) { + const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0); + + if (difference !== 0) { + return difference; + } + } + + return 0; +} diff --git a/toju-app/src/app/domains/plugins/index.ts b/toju-app/src/app/domains/plugins/index.ts new file mode 100644 index 0000000..a20bdb6 --- /dev/null +++ b/toju-app/src/app/domains/plugins/index.ts @@ -0,0 +1,19 @@ +export * from './application/services/plugin-capability.service'; +export * from './application/services/plugin-bootstrap.service'; +export * from './application/services/plugin-client-api.service'; +export * from './application/services/plugin-desktop-state.service'; +export * from './application/services/plugin-host.service'; +export * from './application/services/plugin-logger.service'; +export * from './application/services/plugin-message-bus.service'; +export * from './application/services/plugin-registry.service'; +export * from './application/services/plugin-requirement.service'; +export * from './application/services/plugin-requirement-state.service'; +export * from './application/services/plugin-storage.service'; +export * from './application/services/plugin-store.service'; +export * from './application/services/plugin-ui-registry.service'; +export * from './domain/logic/plugin-dependency-resolver.logic'; +export * from './domain/logic/plugin-manifest-validation.logic'; +export * from './domain/models/plugin-api.models'; +export * from './domain/models/plugin-runtime.models'; +export * from './domain/models/plugin-store.models'; +export * from './infrastructure/local-plugin-discovery.service'; diff --git a/toju-app/src/app/domains/plugins/infrastructure/local-plugin-discovery.service.spec.ts b/toju-app/src/app/domains/plugins/infrastructure/local-plugin-discovery.service.spec.ts new file mode 100644 index 0000000..30232a1 --- /dev/null +++ b/toju-app/src/app/domains/plugins/infrastructure/local-plugin-discovery.service.spec.ts @@ -0,0 +1,114 @@ +import { Injector } from '@angular/core'; +import type { ElectronApi } from '../../../core/platform/electron/electron-api.models'; +import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service'; +import type { TojuPluginManifest } from '../../../shared-kernel'; +import { LocalPluginDiscoveryService } from './local-plugin-discovery.service'; + +const TEST_PLUGIN_MANIFEST = createTestPluginManifest(); + +describe('LocalPluginDiscoveryService', () => { + let electronApi: ElectronApi | null; + + beforeEach(() => { + electronApi = null; + }); + + it('returns a safe empty result outside Electron', async () => { + const service = createDiscoveryService(() => electronApi); + + expect(service.isAvailable).toBe(false); + await expect(service.getPluginsPath()).resolves.toBeNull(); + await expect(service.discoverManifests()).resolves.toEqual({ + errors: [], + plugins: [], + pluginsPath: '' + }); + }); + + it('maps Electron discovery results into plugin runtime models', async () => { + electronApi = { + getLocalPluginsPath: vi.fn(async () => '/plugins'), + listLocalPluginManifests: vi.fn(async () => ({ + errors: [], + plugins: [ + { + discoveredAt: 1, + entrypointPath: '/plugins/api-test-plugin/dist/main.js', + manifest: TEST_PLUGIN_MANIFEST, + manifestPath: '/plugins/api-test-plugin/toju-plugin.json', + pluginRoot: '/plugins/api-test-plugin', + readmePath: '/plugins/api-test-plugin/README.md' + } + ], + pluginsPath: '/plugins' + })) + } as Partial as ElectronApi; + + const service = createDiscoveryService(() => electronApi); + + expect(service.isAvailable).toBe(true); + await expect(service.getPluginsPath()).resolves.toBe('/plugins'); + await expect(service.discoverManifests()).resolves.toEqual({ + errors: [], + plugins: [ + { + discoveredAt: 1, + entrypointPath: '/plugins/api-test-plugin/dist/main.js', + manifest: TEST_PLUGIN_MANIFEST, + manifestPath: '/plugins/api-test-plugin/toju-plugin.json', + pluginRoot: '/plugins/api-test-plugin', + readmePath: '/plugins/api-test-plugin/README.md' + } + ], + pluginsPath: '/plugins' + }); + }); +}); + +function createDiscoveryService(readElectronApi: () => ElectronApi | null): LocalPluginDiscoveryService { + const injector = Injector.create({ + providers: [ + LocalPluginDiscoveryService, + { + provide: ElectronBridgeService, + useValue: { + get isAvailable(): boolean { + return readElectronApi() !== null; + }, + getApi: vi.fn(() => readElectronApi()) + } + } + ] + }); + + return injector.get(LocalPluginDiscoveryService); +} + +function createTestPluginManifest(): TojuPluginManifest { + return { + apiVersion: '1.0.0', + capabilities: [ + 'storage.serverData.read', + 'storage.serverData.write', + 'events.server.publish' + ], + compatibility: { + minimumTojuVersion: '1.0.0' + }, + description: 'Fixture plugin used by automated tests for plugin support APIs.', + entrypoint: './dist/main.js', + events: [ + { + direction: 'serverRelay', + eventName: 'e2e:relay', + maxPayloadBytes: 2048, + scope: 'server' + } + ], + id: 'e2e.plugin-api', + kind: 'client', + schemaVersion: 1, + title: 'E2E Plugin API Fixture', + version: '1.0.0' + }; +} diff --git a/toju-app/src/app/domains/plugins/infrastructure/local-plugin-discovery.service.ts b/toju-app/src/app/domains/plugins/infrastructure/local-plugin-discovery.service.ts new file mode 100644 index 0000000..aae98ed --- /dev/null +++ b/toju-app/src/app/domains/plugins/infrastructure/local-plugin-discovery.service.ts @@ -0,0 +1,46 @@ +import { Injectable, inject } from '@angular/core'; +import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service'; +import type { LocalPluginDiscoveryResult, LocalPluginManifestDescriptor } from '../domain/models/plugin-runtime.models'; + +@Injectable({ providedIn: 'root' }) +export class LocalPluginDiscoveryService { + private readonly electronBridge = inject(ElectronBridgeService); + + get isAvailable(): boolean { + return this.electronBridge.isAvailable; + } + + async getPluginsPath(): Promise { + const api = this.electronBridge.getApi(); + + return api ? await api.getLocalPluginsPath() : null; + } + + async discoverManifests(): Promise { + const api = this.electronBridge.getApi(); + + if (!api) { + return { + errors: [], + plugins: [], + pluginsPath: '' + }; + } + + const result = await api.listLocalPluginManifests(); + + return { + errors: result.errors, + plugins: result.plugins.map((plugin): LocalPluginManifestDescriptor => ({ + discoveredAt: plugin.discoveredAt, + entrypointPath: plugin.entrypointPath, + pluginRootUrl: plugin.pluginRootUrl, + manifest: plugin.manifest, + manifestPath: plugin.manifestPath, + pluginRoot: plugin.pluginRoot, + readmePath: plugin.readmePath + })), + pluginsPath: result.pluginsPath + }; + } +} diff --git a/toju-app/src/app/domains/server-directory/README.md b/toju-app/src/app/domains/server-directory/README.md index 38ebeda..ab89f3f 100644 --- a/toju-app/src/app/domains/server-directory/README.md +++ b/toju-app/src/app/domains/server-directory/README.md @@ -153,12 +153,16 @@ The API service normalises every `ServerInfo` response, filling in `sourceId`, ` That search fan-out is discovery only. Once a room is created or joined, the room keeps an authoritative signal-server affinity via its `sourceId` / `sourceUrl`. The join response can repair stale saved metadata, and reconnect logic now retries that authoritative endpoint first before probing any other configured endpoints. +The `/search` My Servers row and the server rail both read from the active user's local room ownership. Switching accounts reloads that scoped cache so joined servers and local history do not bleed between users. + Fallback stays temporary. If the authoritative endpoint is unavailable, the client can probe other active compatible endpoints as a last resort for the current session, but it does not rewrite the room's saved affinity to that fallback endpoint. ## Server-owned room metadata `ServerInfo` also carries the server-owned `channels` list for each room. Register and update calls persist this channel metadata on the server, and search or hydration responses return the normalised channel list so text and voice channel topology survives reloads, reconnects, and fresh joins. +Server icons are uploaded through the server settings page. Static sources are drawn into a `64x64` canvas and encoded using the smallest browser-supported output among WebP, JPEG, and PNG. Small animated GIF/WebP icons are kept animated. Server icon UI surfaces render the image as a CSS background instead of an `` element so the icon cannot be dragged out of the app. + The renderer may cache room data locally, but channel creation, rename, and removal must round-trip through the server-directory API instead of being treated as client-only state. Server-side normalisation deduplicates channel names within each channel type, so a text `general` channel and a voice `General` channel can coexist while duplicate voice-to-voice or text-to-text names are still rejected. ## Default endpoint management diff --git a/toju-app/src/app/domains/server-directory/domain/models/server-directory.model.ts b/toju-app/src/app/domains/server-directory/domain/models/server-directory.model.ts index 8082268..61ccd04 100644 --- a/toju-app/src/app/domains/server-directory/domain/models/server-directory.model.ts +++ b/toju-app/src/app/domains/server-directory/domain/models/server-directory.model.ts @@ -18,6 +18,8 @@ export interface ServerInfo { ownerPublicKey?: string; userCount: number; maxUsers: number; + icon?: string; + iconUpdatedAt?: number; hasPassword?: boolean; isPrivate: boolean; tags?: string[]; diff --git a/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.html b/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.html index 8b29190..f34494b 100644 --- a/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.html +++ b/toju-app/src/app/domains/server-directory/feature/server-search/server-search.component.html @@ -101,8 +101,16 @@ (dblclick)="openServerCard(server)" >
-
- {{ server.name[0] || '?' }} +
+ @if (server.icon) { + + } @else { + {{ server.name[0] || '?' }} + }
@@ -289,6 +297,96 @@ } +@if (pluginConsentDialog(); as dialog) { + + +} + @if (showCreateDialog()) {
(); private banLookupRequestVersion = 0; @@ -102,6 +118,10 @@ export class ServerSearchComponent implements OnInit { joinErrorMessage = signal(null); joinedServerMenuId = signal(null); leaveDialogRoom = signal(null); + pluginConsentDialog = signal(null); + selectedOptionalPluginIds = signal>(new Set()); + pluginConsentBusy = signal(false); + pluginConsentError = signal(null); // Create dialog state showCreateDialog = signal(false); @@ -118,6 +138,7 @@ export class ServerSearchComponent implements OnInit { const currentUser = this.currentUser(); void this.refreshBannedLookup(servers, currentUser ?? null); + void this.requestMissingServerIcons(servers, currentUser ?? null); }); } @@ -225,7 +246,7 @@ export class ServerSearchComponent implements OnInit { toggleJoinedServerMenu(event: Event, server: ServerInfo): void { event.stopPropagation(); - this.joinedServerMenuId.update((currentId) => currentId === server.id ? null : server.id); + this.joinedServerMenuId.update((currentId) => (currentId === server.id ? null : server.id)); } closeJoinedServerMenu(): void { @@ -255,10 +276,12 @@ export class ServerSearchComponent implements OnInit { return; } - this.store.dispatch(RoomsActions.forgetRoom({ - roomId: room.id, - nextOwnerKey: result.nextOwnerKey - })); + this.store.dispatch( + RoomsActions.forgetRoom({ + roomId: room.id, + nextOwnerKey: result.nextOwnerKey + }) + ); this.leaveDialogRoom.set(null); } @@ -275,6 +298,60 @@ export class ServerSearchComponent implements OnInit { this.joinPasswordError.set(null); } + closePluginConsentDialog(): void { + if (this.pluginConsentBusy()) { + return; + } + + this.pluginConsentDialog.set(null); + this.selectedOptionalPluginIds.set(new Set()); + this.pluginConsentError.set(null); + } + + toggleOptionalPluginInstall(pluginId: string, checked: boolean): void { + this.selectedOptionalPluginIds.update((selectedIds) => { + const nextIds = new Set(selectedIds); + + if (checked) { + nextIds.add(pluginId); + } else { + nextIds.delete(pluginId); + } + + return nextIds; + }); + } + + async confirmPluginConsent(): Promise { + const dialog = this.pluginConsentDialog(); + + if (!dialog) { + return; + } + + const selectedOptionalIds = this.selectedOptionalPluginIds(); + const acceptedRequirements = dialog.required.concat( + dialog.optional.filter((requirement) => selectedOptionalIds.has(requirement.pluginId)) + ); + + this.pluginConsentBusy.set(true); + this.pluginConsentError.set(null); + + try { + await this.attemptJoinServer(dialog.server, dialog.password, { + acceptedRequirements, + skipPluginConsent: true + }); + + this.pluginConsentDialog.set(null); + this.selectedOptionalPluginIds.set(new Set()); + } catch (error) { + this.pluginConsentError.set(error instanceof Error ? error.message : 'Unable to install server plugins'); + } finally { + this.pluginConsentBusy.set(false); + } + } + async confirmPasswordJoin(): Promise { const server = this.passwordPromptServer(); @@ -304,9 +381,7 @@ export class ServerSearchComponent implements OnInit { getServerOwnerLabel(server: ServerInfo): string { const joinedRoom = this.joinedRoomForServer(server); const ownerKey = server.ownerId || joinedRoom?.hostId || ''; - const ownerMember = joinedRoom?.members?.find((member) => - member.id === ownerKey || member.oderId === ownerKey - ); + const ownerMember = joinedRoom?.members?.find((member) => member.id === ownerKey || member.oderId === ownerKey); return server.ownerName || ownerMember?.displayName || server.ownerId || joinedRoom?.hostId || 'Unknown owner'; } @@ -324,6 +399,8 @@ export class ServerSearchComponent implements OnInit { hostName: room.hostId || 'Unknown', userCount: room.userCount ?? 0, maxUsers: room.maxUsers ?? 50, + icon: room.icon, + iconUpdatedAt: room.iconUpdatedAt, hasPassword: typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password, isPrivate: room.isPrivate, channels: room.channels, @@ -335,7 +412,11 @@ export class ServerSearchComponent implements OnInit { }; } - private async attemptJoinServer(server: ServerInfo, password?: string): Promise { + private async attemptJoinServer( + server: ServerInfo, + password?: string, + options: { acceptedRequirements?: PluginRequirementSummary[]; skipPluginConsent?: boolean } = {} + ): Promise { const currentUserId = localStorage.getItem('metoyou_currentUserId'); const currentUser = this.currentUser(); @@ -348,37 +429,57 @@ export class ServerSearchComponent implements OnInit { this.joinPasswordError.set(null); try { - const response = await firstValueFrom(this.serverDirectory.requestJoin({ - roomId: server.id, - userId: currentUserId, - userPublicKey: currentUser?.oderId || currentUserId, - displayName: currentUser?.displayName || 'Anonymous', - password: password?.trim() || undefined - }, { - sourceId: server.sourceId, - sourceUrl: server.sourceUrl - })); - const resolvedSource = this.serverDirectory.normaliseRoomSignalSource({ - sourceId: response.server.sourceId ?? server.sourceId, - sourceName: response.server.sourceName ?? server.sourceName, - sourceUrl: response.server.sourceUrl ?? server.sourceUrl, - signalingUrl: response.signalingUrl, - fallbackName: response.server.sourceName ?? server.sourceName ?? server.name - }, { - ensureEndpoint: true - }); + if (options.skipPluginConsent !== true) { + const consentDialog = await this.buildPluginConsentDialog(server, password); + + if (consentDialog) { + this.pluginConsentDialog.set(consentDialog); + this.selectedOptionalPluginIds.set(new Set(consentDialog.optional.map((requirement) => requirement.pluginId))); + return; + } + } + + const response = await firstValueFrom( + this.serverDirectory.requestJoin( + { + roomId: server.id, + userId: currentUserId, + userPublicKey: currentUser?.oderId || currentUserId, + displayName: currentUser?.displayName || 'Anonymous', + password: password?.trim() || undefined + }, + { + sourceId: server.sourceId, + sourceUrl: server.sourceUrl + } + ) + ); + const resolvedSource = this.serverDirectory.normaliseRoomSignalSource( + { + sourceId: response.server.sourceId ?? server.sourceId, + sourceName: response.server.sourceName ?? server.sourceName, + sourceUrl: response.server.sourceUrl ?? server.sourceUrl, + signalingUrl: response.signalingUrl, + fallbackName: response.server.sourceName ?? server.sourceName ?? server.name + }, + { + ensureEndpoint: true + } + ); const resolvedServer = { ...server, ...response.server, - channels: - Array.isArray(response.server.channels) && response.server.channels.length > 0 - ? response.server.channels - : server.channels, + channels: Array.isArray(response.server.channels) && response.server.channels.length > 0 ? response.server.channels : server.channels, ...resolvedSource, signalingUrl: response.signalingUrl }; this.closePasswordDialog(); + + if (options.acceptedRequirements?.length) { + await this.pluginStore.installServerRequirementsLocally(resolvedServer.id, options.acceptedRequirements, { activate: true }); + } + this.store.dispatch( RoomsActions.joinRoom({ roomId: resolvedServer.id, @@ -406,6 +507,83 @@ export class ServerSearchComponent implements OnInit { } this.joinErrorMessage.set(message); + + if (options.skipPluginConsent) { + throw new Error(message); + } + } + } + + private async buildPluginConsentDialog(server: ServerInfo, password?: string): Promise { + const apiBaseUrl = this.serverDirectory.getApiBaseUrl({ + sourceId: server.sourceId, + sourceUrl: server.sourceUrl + }); + const snapshot = await firstValueFrom(this.pluginRequirements.getSnapshot(apiBaseUrl, server.id)); + const installedPluginIds = await this.pluginStore.getLocalServerInstalledPluginIds(server.id); + const installableRequirements = snapshot.requirements + .filter((requirement) => !installedPluginIds.has(requirement.pluginId)) + .filter((requirement) => !!requirement.manifest || !!requirement.installUrl); + const required = installableRequirements.filter((requirement) => requirement.status === 'required'); + const optional = installableRequirements.filter( + (requirement) => requirement.status === 'optional' || requirement.status === 'recommended' + ); + + if (required.length === 0 && optional.length === 0) { + return null; + } + + return { + optional, + password, + required, + server + }; + } + + private async requestMissingServerIcons(servers: ServerInfo[], currentUser: User | null): Promise { + if (!currentUser) { + return; + } + + for (const server of servers) { + if (server.icon) { + continue; + } + + const selector = this.serverDirectory.buildRoomSignalSelector( + { + sourceId: server.sourceId, + sourceName: server.sourceName, + sourceUrl: server.sourceUrl, + fallbackName: server.sourceName ?? server.name + }, + { + ensureEndpoint: !!server.sourceUrl + } + ); + + if (!selector) { + continue; + } + + const wsUrl = this.serverDirectory.getWebSocketUrl(selector); + + try { + await firstValueFrom(this.webrtc.connectToSignalingServer(wsUrl)); + this.webrtc.identify(currentUser.oderId || currentUser.id, currentUser.displayName || 'User', wsUrl, { + description: currentUser.description, + profileUpdatedAt: currentUser.profileUpdatedAt + }); + + this.webrtc.sendRawMessageToSignalUrl(wsUrl, { + type: 'server_icon_sync_request', + serverId: server.id, + iconUpdatedAt: 0 + }); + } catch { + /* discovery icons are best-effort */ + } } } diff --git a/toju-app/src/app/domains/server-directory/infrastructure/services/server-icon-image.service.ts b/toju-app/src/app/domains/server-directory/infrastructure/services/server-icon-image.service.ts new file mode 100644 index 0000000..5894525 --- /dev/null +++ b/toju-app/src/app/domains/server-directory/infrastructure/services/server-icon-image.service.ts @@ -0,0 +1,146 @@ +import { Injectable } from '@angular/core'; +import { isAnimatedGif, isAnimatedWebp } from '../../../profile-avatar/infrastructure/services/profile-avatar-image.service'; + +export interface ProcessedServerIcon { + dataUrl: string; + mime: string; + size: number; +} + +const SERVER_ICON_SIZE = 64; +const STATIC_ICON_CANDIDATES = [ + { mime: 'image/webp', quality: 0.82 }, + { mime: 'image/jpeg', quality: 0.82 }, + { mime: 'image/png' } +]; + +@Injectable({ providedIn: 'root' }) +export class ServerIconImageService { + async process(file: File): Promise { + if (!file.type.startsWith('image/')) { + throw new Error('Choose an image file.'); + } + + const objectUrl = URL.createObjectURL(file); + + try { + const image = await this.loadImage(objectUrl); + const isAnimated = await this.isAnimated(file); + + if (isAnimated && image.naturalWidth <= SERVER_ICON_SIZE && image.naturalHeight <= SERVER_ICON_SIZE) { + const dataUrl = await this.readBlobAsDataUrl(file); + + return { + dataUrl, + mime: file.type || this.resolveMimeFromDataUrl(dataUrl), + size: file.size + }; + } + + return await this.renderStaticIcon(image); + } finally { + URL.revokeObjectURL(objectUrl); + } + } + + private async renderStaticIcon(image: HTMLImageElement): Promise { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + if (!context) { + throw new Error('Canvas not supported.'); + } + + canvas.width = SERVER_ICON_SIZE; + canvas.height = SERVER_ICON_SIZE; + + const scale = Math.max(SERVER_ICON_SIZE / image.naturalWidth, SERVER_ICON_SIZE / image.naturalHeight); + const drawWidth = image.naturalWidth * scale; + const drawHeight = image.naturalHeight * scale; + const drawX = (SERVER_ICON_SIZE - drawWidth) / 2; + const drawY = (SERVER_ICON_SIZE - drawHeight) / 2; + + context.clearRect(0, 0, SERVER_ICON_SIZE, SERVER_ICON_SIZE); + context.imageSmoothingEnabled = true; + context.imageSmoothingQuality = 'high'; + context.drawImage(image, drawX, drawY, drawWidth, drawHeight); + + const candidates = await Promise.all( + STATIC_ICON_CANDIDATES.map(async (candidate) => { + const blob = await this.canvasToBlob(canvas, candidate.mime, candidate.quality); + const dataUrl = await this.readBlobAsDataUrl(blob); + + return { + dataUrl, + mime: blob.type || candidate.mime, + size: blob.size + }; + }) + ); + + return candidates.reduce((smallest, candidate) => (candidate.size < smallest.size ? candidate : smallest)); + } + + private async isAnimated(file: File): Promise { + const mime = file.type.toLowerCase(); + + if (mime !== 'image/gif' && mime !== 'image/webp') { + return false; + } + + const buffer = await file.arrayBuffer(); + + return mime === 'image/gif' ? isAnimatedGif(buffer) : isAnimatedWebp(buffer); + } + + private canvasToBlob(canvas: HTMLCanvasElement, type: string, quality?: number): Promise { + return new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => { + if (blob) { + resolve(blob); + return; + } + + reject(new Error('Failed to render server image.')); + }, + type, + quality + ); + }); + } + + private readBlobAsDataUrl(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + return; + } + + reject(new Error('Failed to encode server image.')); + }; + + reader.onerror = () => reject(reader.error ?? new Error('Failed to read server image.')); + reader.readAsDataURL(blob); + }); + } + + private loadImage(url: string): Promise { + return new Promise((resolve, reject) => { + const image = new Image(); + + image.onload = () => resolve(image); + image.onerror = () => reject(new Error('Failed to load server image.')); + image.src = url; + }); + } + + private resolveMimeFromDataUrl(dataUrl: string): string { + const match = /^data:([^;,]+)/.exec(dataUrl); + + return match?.[1] || 'image/webp'; + } +} diff --git a/toju-app/src/app/domains/theme/feature/theme-style-application.logic.spec.ts b/toju-app/src/app/domains/theme/feature/theme-style-application.logic.spec.ts index a4de8ef..fa4cb14 100644 --- a/toju-app/src/app/domains/theme/feature/theme-style-application.logic.spec.ts +++ b/toju-app/src/app/domains/theme/feature/theme-style-application.logic.spec.ts @@ -1,7 +1,4 @@ -import { - applyThemeStyleDeclaration, - toCssStylePropertyName -} from './theme-style-application.logic'; +import { applyThemeStyleDeclaration, toCssStylePropertyName } from './theme-style-application.logic'; describe('theme style application', () => { it('applies camelCase theme properties as real CSS declarations', () => { diff --git a/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.html b/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.html index 0980749..d06e79e 100644 --- a/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.html +++ b/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.html @@ -16,13 +16,11 @@ class="w-3.5 h-3.5" /> @if (voiceSession()?.serverIcon) { - + } @else {
{{ voiceSession()?.serverName?.charAt(0)?.toUpperCase() || '?' }} diff --git a/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.ts b/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.ts index 7aab098..fdb7f83 100644 --- a/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.ts +++ b/toju-app/src/app/domains/voice-session/feature/floating-voice-controls/floating-voice-controls.component.ts @@ -6,7 +6,7 @@ import { computed, OnInit } from '@angular/core'; -import { CommonModule, NgOptimizedImage } from '@angular/common'; +import { CommonModule } from '@angular/common'; import { Store } from '@ngrx/store'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { @@ -34,7 +34,6 @@ import { ThemeNodeDirective } from '../../../../domains/theme'; standalone: true, imports: [ CommonModule, - NgOptimizedImage, NgIcon, DebugConsoleComponent, ScreenShareQualityDialogComponent, diff --git a/toju-app/src/app/features/room/rooms-side-panel/rooms-side-panel.component.html b/toju-app/src/app/features/room/rooms-side-panel/rooms-side-panel.component.html index 4e952e7..5905b52 100644 --- a/toju-app/src/app/features/room/rooms-side-panel/rooms-side-panel.component.html +++ b/toju-app/src/app/features/room/rooms-side-panel/rooms-side-panel.component.html @@ -2,12 +2,20 @@