diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9c922ef --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,59 @@ +# AGENTS.md — Gadget Code Monorepo + +## Project Structure + +``` +gadget/ +├── packages/ai/ @gadget/ai — AI API abstraction (source of truth) +├── gadget-code/ Web service + browser IDE +├── gadget-drone/ Worker process (agentic workflow loop) +└── pnpm-workspace.yaml Monorepo workspace definition +``` + +## Rules for AI Code + +The ecosystem speaks **one** AI API language defined by `@gadget/ai`. All rules apply to every consumer: + +1. **No direct SDK imports.** Never `import { Ollama } from "ollama"` or `import OpenAI from "openai"` outside `packages/ai/src/`. +2. **No `provider.sdk` checks outside the factory.** The factory in `packages/ai/src/index.ts` routes to the correct implementation. After `createAiApi()`, the caller holds an `AiApi` and never checks which SDK backs it. +3. **All AI interfaces live in `@gadget/ai`.** Do not re-declare `IAiProvider`, `IAiChatResponse`, etc. in consumer packages. Import them from `@gadget/ai`. +4. **Adding a new provider** means implementing `AiApi` in `packages/ai/src/` and registering it in the factory. Nothing else changes. + +## Dev Commands + +```bash +pnpm install # Install all workspace packages +pnpm -r build # Build all packages +pnpm --filter @gadget/ai build +pnpm --filter gadget-drone build +pnpm --filter gadget-code build:backend +pnpm --filter gadget-code dev +pnpm --filter gadget-drone dev +``` + +## Key Conventions + +- All packages are **ES modules** (`"type": "module"`). +- `@gadget/ai` uses `moduleResolution: NodeNext` and emits to `dist/`. +- gadget-drone uses `moduleResolution: NodeNext`. +- gadget-code uses `moduleResolution: bundler` with `@/*` path aliases. +- Dependency versions are pinned in `package.json` — no ranges. Use the workspace protocol (`workspace:*`) for internal package references. +- pnpm version is enforced at the workspace root (`packageManager` field). + +## TypeScript Strictness + +| Package | Strictness | +|---|---| +| `@gadget/ai` | `strict: true` | +| gadget-drone | `strict: true` | +| gadget-code | `strict: true`, `noUnusedLocals`, `noUnusedParameters`, `noUncheckedIndexedAccess` | + +## Adding Code + +When adding a new feature or service, determine its scope: + +- **Shared concern** (AI, logging, config schema) → goes in `@gadget/ai` +- **Drone-only** (Bull queue, workspace file operations) → goes in gadget-drone +- **Web-only** (Express routes, Mongoose models, session management) → goes in gadget-code + +If code is needed by both consumer packages, it belongs in `@gadget/ai`. Do not copy-paste shared logic across packages. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..98eb99e --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Gadget Code + +A self-hosted **Agentic Engineering Platform (AEP)** — an IDE that drives autonomous AI agents to perform software engineering work on your behalf, running in your own environment. + +## Projects + +| Package | Role | +|---|---| +| `gadget-code` | Web service — agentic IDE, browser UI, API server | +| `gadget-drone` | Worker process — runs the agentic workflow loop in workspace directories | +| `@gadget/ai` | Shared AI API abstraction — Ollama and OpenAI, called by both | + +## AI API Abstraction (`@gadget/ai`) + +All AI API calls throughout the Gadget Code ecosystem route through `@gadget/ai`. No consumer code imports Ollama or OpenAI SDKs directly. No consumer code checks `provider.sdk` after the factory call. The shared module translates Gadget Code's internal API contract into whatever provider is configured, and translates responses back to Gadget Code's internal types. + +See `packages/ai/README.md` for the full API reference. + +## Setup + +```bash +pnpm install +``` + +## Build + +```bash +pnpm -r build # build all packages +pnpm --filter @gadget/ai build +pnpm --filter gadget-drone build +pnpm --filter gadget-code build:backend +``` + +## Run + +```bash +# Backend +pnpm --filter gadget-code dev + +# Drone worker (in a project workspace directory) +pnpm --filter gadget-drone dev +``` + +## Architecture + +gadget-code runs on server infrastructure (MongoDB, Redis, etc.) and serves the browser-based IDE. gadget-drone runs on end-user machines, connecting via WebSocket to gadget-code, and executes the agentic workflow loop against local project directories via remote control. gadget-drone never connects directly to MongoDB or Redis — it communicates entirely through the Gadget Code API. + +AI calls are handled by `@gadget/ai`, which both projects depend on. This keeps all AI SDK knowledge in one place. \ No newline at end of file diff --git a/gadget-code/.gitignore b/gadget-code/.gitignore new file mode 100644 index 0000000..e62f8eb --- /dev/null +++ b/gadget-code/.gitignore @@ -0,0 +1,12 @@ +.env + +data/minio +ssl/*cnf +ssl/*csr +ssl/*srl +ssl/*crt +ssl/*key + +dist +node_modules +test-results diff --git a/gadget-code/.vscode/launch.json b/gadget-code/.vscode/launch.json new file mode 100644 index 0000000..5214070 --- /dev/null +++ b/gadget-code/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "app", + "skipFiles": ["/**"], + "outFiles": [], + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", + "program": "${workspaceFolder}/src/web-app.ts", + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "cli", + "skipFiles": ["/**"], + "outFiles": [], + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", + "program": "${workspaceFolder}/src/web-cli.ts", + "console": "integratedTerminal" + } + ] +} diff --git a/gadget-code/AGENTS.md b/gadget-code/AGENTS.md new file mode 100644 index 0000000..546047d --- /dev/null +++ b/gadget-code/AGENTS.md @@ -0,0 +1,35 @@ +# AGENTS.md + +## Quick Commands +```bash +pnpm dev:backend # Backend on https://localhost:3443 +pnpm dev:frontend # Frontend on https://localhost:5174 +pnpm build # Build backend -> dist/ + frontend +pnpm test # Vitest unit tests +npx playwright test # E2E tests (requires running backend + frontend) +``` + +## Prerequisites +- Node.js 22+, pnpm 10+ +- MongoDB on localhost:27017 +- Redis on localhost:6379 +- SSL certificates in `ssl/` directory + +## Path Aliases +`@/*` maps to `src/*` (defined in tsconfig.json) + +## Test quirks +- Vitest runs unit tests in `tests/**/*.test.ts` excluding `tests/e2e/` +- Playwright e2e tests target `https://code-dev.g4dge7.com:5174` (requires running dev servers) + +## Build output +- Backend compiles to `dist/` (TypeScript) +- Frontend builds to `frontend/dist/` + +## TypeScript strictness +strict, noUnusedLocals, noUnusedParameters, noUncheckedIndexedAccess all enabled + +## Architecture +- Backend: Express 5 + Socket.io + Mongoose + Redis sessions +- Frontend: React 19 + Vite 8 + Tailwind CSS 4 +- Entry points: `src/web-app.ts` (backend), `frontend/src/main.tsx` (frontend) \ No newline at end of file diff --git a/gadget-code/LICENSE b/gadget-code/LICENSE new file mode 100644 index 0000000..933fca5 --- /dev/null +++ b/gadget-code/LICENSE @@ -0,0 +1,203 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other + modifications represent, as a whole, an original work of + authorship. For the purposes of this License, Derivative Works + shall not include works that remain separable from, or merely + link (or bind by name) to the interfaces of, the Work and + Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, + the Licensor for the purpose of discussing and improving the Work, + but excluding communication that is conspicuously marked or + otherwise designated in writing by the copyright owner + as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license(s) to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright (C) 2026 Robert Colbert + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/gadget-code/README.md b/gadget-code/README.md new file mode 100644 index 0000000..32f6601 --- /dev/null +++ b/gadget-code/README.md @@ -0,0 +1,165 @@ +# Gadget Code + +A modern self-hosted and open source Agentic Engineering Environment. Gadget Code is a hackable TypeScript/Node.js web application - and application development framework - built with Express, React, Vite, and Tailwind CSS. + +Gadget Code is a Gab-first initiative. It is most well-tested and tuned for use with [Gab.ai](https://gab.ai) as your "Big Brain" compute provider. However, it is designed to be easily adaptable for use with other compute providers as well as your own local Ollama, vLLM, or other OpenAI _or_ Ollama API-compatible setup. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Vite Dev Server (HTTPS) │ +│ https://code-dev.g4dge7.com:5174 │ +│ │ +│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ +│ │ /auth │ │ /api │ │ static │ │ +│ │ proxy │ │ proxy │ │ files │ │ +│ └────┬──────┘ └────┬──────┘ └───────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────┐ │ +│ │ Backend (HTTP) │ │ +│ │ http://localhost:3443 │ │ +│ │ - Express API │ │ +│ │ - Socket.io │ │ +│ │ - Session (Redis) │ │ +│ │ - MongoDB │ │ +│ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Tech Stack + +- **Frontend**: React 19, Vite 8, Tailwind CSS 4, Socket.io Client +- **Backend**: Express 5, TypeScript, Socket.io Server +- **Database**: MongoDB (Mongoose) +- **Cache/Session**: Redis (ioredis + connect-redis) +- **File Storage**: MinIO +- **Testing**: Vitest, Playwright + +## Getting Started + +### Prerequisites + +- Node.js 22+ +- pnpm 10+ +- MongoDB running on localhost:27017 +- Redis running on localhost:6379 +- SSL certificates in `ssl/` directory + +### Installation + +```bash +pnpm install +``` + +### Development + +```bash +# Start both backend and frontend +pnpm dev:backend # Backend on http://localhost:3443 +pnpm dev:frontend # Frontend on https://localhost:5174 +``` + +### Build + +```bash +pnpm build # Build backend + frontend +pnpm build:backend # Build TypeScript only +pnpm build:frontend # Build React/Vite only +``` + +### Testing + +```bash +pnpm test # Run vitest unit tests +npx playwright test # Run E2E integration tests +``` + +## Scripts + +Your environment can be managed with the following package.json scripts and npx commands. + +| Command | Description | +| --------------------- | -------------------------- | +| `pnpm build` | Build backend + frontend | +| `pnpm test` | Run vitest unit tests | +| `npx playwright test` | Run E2E tests | +| `pnpm dev:backend` | Start backend (port 3443) | +| `pnpm dev:frontend` | Start frontend (port 5174) | +| `pnpm start` | Start production build | + +## Environment Variables + +```env +# Required +DTP_USER_PASSWORD_SALT="your-password-salt" +DTP_JWT_SECRET="your-jwt-secret" +DTP_SESSION_SECRET="your-session-secret" + +# MongoDB +DTP_MONGODB_HOST="localhost" +DTP_MONGODB_DATABASE="gadget-code" + +# Redis +DTP_REDIS_HOST="localhost" +DTP_REDIS_PORT="6379" + +# HTTPS +DTP_HTTPS="enabled" +DTP_HTTPS_HOST="localhost" +DTP_HTTPS_PORT="3443" +DTP_HTTPS_KEY_FILE="/path/to/ssl/key.pem" +DTP_HTTPS_CRT_FILE="/path/to/ssl/cert.pem" + +# Session +DTP_SESSION_TRUST_PROXY="enabled" +DTP_SESSION_COOKIE_SECURE="enabled" +DTP_SESSION_COOKIE_SAMESITE="strict" + +# User Signup +DTP_USER_SIGNUP="enabled" +``` + +## Features + +- User authentication (sign up, sign in, sign out) +- JWT-based session management +- RESTful API structure +- Socket.io real-time communication +- File upload support (MinIO) +- Email notifications +- Tailwind CSS frontend with dark theme + +## Project Structure + +``` +/src + /config # Environment configuration + /controllers # Express route handlers + /lib # Core utilities + /models # Mongoose models + /services # Business logic +/frontend + /src + /components # React components + /pages # React pages + /lib # Frontend utilities + /public # Static assets + vite.config.ts # Vite configuration +/tests + /e2e # Playwright tests +``` + +## API Endpoints + +| Method | Endpoint | Description | +| ------ | ---------------- | ------------------ | +| POST | `/auth/sign-up` | Create new account | +| POST | `/auth/sign-in` | Authenticate | +| GET | `/auth/sign-out` | Sign out | +| GET | `/api/v1/user` | Get current user | + +## License + +SEE LICENSE IN LICENSE diff --git a/gadget-code/assets/icon/icon-114x114.png b/gadget-code/assets/icon/icon-114x114.png new file mode 100644 index 0000000..a4c3234 Binary files /dev/null and b/gadget-code/assets/icon/icon-114x114.png differ diff --git a/gadget-code/assets/icon/icon-120x120.png b/gadget-code/assets/icon/icon-120x120.png new file mode 100644 index 0000000..c807e9b Binary files /dev/null and b/gadget-code/assets/icon/icon-120x120.png differ diff --git a/gadget-code/assets/icon/icon-144x144.png b/gadget-code/assets/icon/icon-144x144.png new file mode 100644 index 0000000..d625c2f Binary files /dev/null and b/gadget-code/assets/icon/icon-144x144.png differ diff --git a/gadget-code/assets/icon/icon-152x152.png b/gadget-code/assets/icon/icon-152x152.png new file mode 100644 index 0000000..863ddb7 Binary files /dev/null and b/gadget-code/assets/icon/icon-152x152.png differ diff --git a/gadget-code/assets/icon/icon-16x16.png b/gadget-code/assets/icon/icon-16x16.png new file mode 100644 index 0000000..24e197a Binary files /dev/null and b/gadget-code/assets/icon/icon-16x16.png differ diff --git a/gadget-code/assets/icon/icon-180x180.png b/gadget-code/assets/icon/icon-180x180.png new file mode 100644 index 0000000..88234ae Binary files /dev/null and b/gadget-code/assets/icon/icon-180x180.png differ diff --git a/gadget-code/assets/icon/icon-192x192.png b/gadget-code/assets/icon/icon-192x192.png new file mode 100644 index 0000000..d73137d Binary files /dev/null and b/gadget-code/assets/icon/icon-192x192.png differ diff --git a/gadget-code/assets/icon/icon-256x256.png b/gadget-code/assets/icon/icon-256x256.png new file mode 100644 index 0000000..d095bef Binary files /dev/null and b/gadget-code/assets/icon/icon-256x256.png differ diff --git a/gadget-code/assets/icon/icon-32x32.png b/gadget-code/assets/icon/icon-32x32.png new file mode 100644 index 0000000..da67231 Binary files /dev/null and b/gadget-code/assets/icon/icon-32x32.png differ diff --git a/gadget-code/assets/icon/icon-384x384.png b/gadget-code/assets/icon/icon-384x384.png new file mode 100644 index 0000000..5ae17c8 Binary files /dev/null and b/gadget-code/assets/icon/icon-384x384.png differ diff --git a/gadget-code/assets/icon/icon-48x48.png b/gadget-code/assets/icon/icon-48x48.png new file mode 100644 index 0000000..e470052 Binary files /dev/null and b/gadget-code/assets/icon/icon-48x48.png differ diff --git a/gadget-code/assets/icon/icon-512x512.png b/gadget-code/assets/icon/icon-512x512.png new file mode 100644 index 0000000..bac45f0 Binary files /dev/null and b/gadget-code/assets/icon/icon-512x512.png differ diff --git a/gadget-code/assets/icon/icon-57x57.png b/gadget-code/assets/icon/icon-57x57.png new file mode 100644 index 0000000..ecf56a3 Binary files /dev/null and b/gadget-code/assets/icon/icon-57x57.png differ diff --git a/gadget-code/assets/icon/icon-60x60.png b/gadget-code/assets/icon/icon-60x60.png new file mode 100644 index 0000000..ac88c08 Binary files /dev/null and b/gadget-code/assets/icon/icon-60x60.png differ diff --git a/gadget-code/assets/icon/icon-72x72.png b/gadget-code/assets/icon/icon-72x72.png new file mode 100644 index 0000000..3ef7ef0 Binary files /dev/null and b/gadget-code/assets/icon/icon-72x72.png differ diff --git a/gadget-code/assets/icon/icon-76x76.png b/gadget-code/assets/icon/icon-76x76.png new file mode 100644 index 0000000..e475603 Binary files /dev/null and b/gadget-code/assets/icon/icon-76x76.png differ diff --git a/gadget-code/assets/icon/icon-96x96.png b/gadget-code/assets/icon/icon-96x96.png new file mode 100644 index 0000000..d8311fd Binary files /dev/null and b/gadget-code/assets/icon/icon-96x96.png differ diff --git a/gadget-code/assets/icon/web-app.png b/gadget-code/assets/icon/web-app.png new file mode 100644 index 0000000..8894cbd Binary files /dev/null and b/gadget-code/assets/icon/web-app.png differ diff --git a/gadget-code/data/prompts/agent/build/system.md b/gadget-code/data/prompts/agent/build/system.md new file mode 100644 index 0000000..3b30b52 --- /dev/null +++ b/gadget-code/data/prompts/agent/build/system.md @@ -0,0 +1,39 @@ +## ROLE + +You are Gadget. You are an AI software agent working as a software developer on a team. + +You are working with your teammates in chat, and communicate with them as a fellow software developer. + +You are an expert software developer who is extremely knowledgeable about software architecture, design, user interface and experience, and of course: Writing actual code. + +## SCOPE + +{{scope_block}} + +We are currently in Build mode, which means the User has accepted a plan, and is now instructing you to execute that plan. Your job is to use your tools to write code, run commands, create files, edit existing files, fix bugs, implement features, and deliver working software. You are diligent. You are thorough. You check your work to make sure the tools are working as you expect and that your intended changes are the changes that were made by the tool. You are not afraid to ask for help when you need it using the ask_questions tool. + +Your job is to turn plans into maintainable and tested software solutions that are well-architected. You consider whether a change you are making could have side effects or cause regressions, and will implement and run tests to ensure correct software functionality. You prefer writing and running tests instead of running the application, though you can run the application to make sure it starts - just be sure to use a timeout. Generally, the User will run the application and test it for you. The User will provide log output and other details while iterating with you on the development and implementation of the features you're building in the application. + +## TOOL USAGE + +Use your tools immediately and aggressively. Do not announce tool usage, ask permission, or describe what you're about to do. Just execute. When you need to check a file, read it. When you need to find something, search for it. When you need to verify something, run the command. Explain your reasoning and results after taking action, not before. + +You must remain within the project directory, which is the current working directory. You cannot access files and data outside of the project directory. You WILL NOT author scripts to work around the limitations of your tools. If you genuinely need something from outside the current working directory, ask the User to provide it for you, or for guidance on an alternate approach. Don't get stuck in a loop trying to figure out how to get something you can't have - ask the User to provide it (or an alternate approach) and stop. + +**DO NOT** spawn a subagent as a workaround for lacking tool features, tool failures, and tool errors. Instead, please respond by writing out what you were trying to do, the parameters you used when calling the tool, and the full response you received. Don't work around tool errors. Report faulty tool performance and behavior, then stop. Let the User help determine what to do next. + +## INSTRUCTIONS + +You always provide regular updates to explain your thinking and reasoning while working and calling tools. You always end a turn by summarizing what you did to the User for their review and convenience. When the user sends you a prompt: + +1. Reason about what they're asking for (or asking you to do). +2. Plan out what you're going to do, the actions you're going to take, to satisfy the request and resolve the user's needs. + a. Use the `ask_questions` tool to present the user with questions and up to 3 answers they can choose from (they will have a 4th: Type your own answer, provided by the system). +3. Work in a loop through your plan for this turn resolving the work items that need done until all work items are resolved. + - Verify your work as appropriate to ensure that tools are working as expected. + - If you find yourself in a loop or struggling, stop and ask the User for help. +4. Summarize your work for the User, and ask if they have any questions or comments about your work, or would like any refinements to the results achieved. + +If you encounter a new or unexpected problem while working, you can make use of the `ask_questions` tool to get quick user feedback, or let the turn end by explaining what you've encountered, and what you need assistance with. + +{{subagent_section}} diff --git a/gadget-code/data/prompts/agent/dev/system.md b/gadget-code/data/prompts/agent/dev/system.md new file mode 100644 index 0000000..464ea9f --- /dev/null +++ b/gadget-code/data/prompts/agent/dev/system.md @@ -0,0 +1,56 @@ +## ROLE + +You are Gadget, a software agent working as a software developer in a development session. + +## SCOPE + +{{scope_block}} + +This is the Develop mode - a special mode reserved for working on the Gadget Code agentic programming harness itself (you). + +You are working on the software project that provides you with prompts on behalf of a User. We refer to this as Gadget Code, the agentic harness in which you run. This application implements the agentic workflow loop you use to call tools and get work done. You are working on that. + +You analyze the User's request, then use your knowledge, tools, and skills to help the User accomplish their goals, tasks, or objectives while working in the Gadget Code codebase. + +You are editing the harness that is running your code. A restart of the harness is required for you to see your changes. The User must perform this restart. Do not attempt to kill a running Gadget Code process unless you started it during a test at the request of the User. Otherwise, the User is running the harness to send you commands and enable your agentic work flow. + +You can't expect to observe the results of changes you make within the current turn. You have to let the turn end, and request a harness restart by the User. The User will perform the restart and let you know that they did that, then ask you to proceed - sometimes with additional instructions and input. + +## TOOL USAGE + +Use your tools proactively. When working on the Gadget Code codebase, immediately read files, search for patterns, run tests, and execute commands to accomplish tasks. Do not announce tool usage or ask permission. Just execute and explain results afterward. + +You must remain within the project directory, which is the current working directory. You cannot access files and data outside of the project directory. You WILL NOT author scripts to work around the limitations of your tools. If you genuinely need something from outside the current working directory, ask the User to provide it for you, or for guidance on an alternate approach. Don't get stuck in a loop trying to figure out how to get something you can't have - ask the User to provide it (or an alternate approach) and stop. + +NOTICE: IF YOU EXPERIENCE DIFFICULTY USING ANY TOOLS OR RECEIVE A RESPONSE THAT IS UNEXPECTED OR SEEMS ERRONEOUS (TOOL MALFUNCTION), PLEASE **IMMEDIATELY** DOCUMENT WHAT THE TOOL DID THAT YOU DIDN'T EXPECT - AND STOP. WHEN IN THE DEVELOP MODE, YOU ARE WORKING WITH A DEVELOPER (THE USER) DIRECTLY ON THIS AGENTIC HARNESS (GADGET CODE). WE MAY NEED TO DEBUG OR DIAGNOSE A PROBLEM WITH A TOOL AS WE WORK. + +**DO NOT** spawn a subagent as a workaround for lacking tool features, tool failures, and tool errors. Instead, please respond by writing out what you were trying to do, the parameters you used when calling the tool, and the full response you received. Don't work around tool errors. Report faulty tool performance and behavior, then stop. Let the User help determine what to do next. + +## INSTRUCTIONS + +Work in a loop through the User's request for this turn, resolving the work items that need done until finished, explaining your thinking and reasoning while calling tools and doing your work. + +If you encounter a new or unexpected problem while working, you can make use of the `ask_questions` tool to get user feedback and guidance, or: + +1. Explain what you've encountered. +2. Explain what you need help with, or what you need the User to do. +3. Stop (let the turn end). + +You always end a turn by summarizing what you did to the User for their review and convenience. + +{{subagent_section}} + +## EXPECTED OUTCOMES + +In Develop mode, you will: + +1. Have conversations with the User as needed to gain clarity and understanding. +2. Create and modify existing program source code, configurations, dependencies, etc., as needed to accomplish the User's goals, tasks, or objectives. +3. Create and modify Skills using the Skills tools. +4. Author tests for your code when appropriate and especially when asked by the User unless impossible. + +The above are just common expectations in most planning sessions. You will do other things as needed, determined by either you (Gadget), or by User request. + +The User may ask for other deliverables in planning sessions. Use your tools and skills to deliver the best results you can. + +If something is impossible, explain why it's impossible and stop. Let the User guide you towards a solution or workaround. Turn the session back into a conversation, and work with the User towards their goals without violating the rules. diff --git a/gadget-code/data/prompts/agent/plan/system.md b/gadget-code/data/prompts/agent/plan/system.md new file mode 100644 index 0000000..e35ef5c --- /dev/null +++ b/gadget-code/data/prompts/agent/plan/system.md @@ -0,0 +1,48 @@ +## ROLE + +You are Gadget. You are a software agent working as a software developer attending a planning session. + +## SCOPE + +{{scope_block}} + +We are currently in Plan mode, which means you are working back & forth with the User to define the work that will be done next. The user could be asking you to help them plan for actions to be taken in any other session mode. Do your best to help them break problems down into solveable chunks, write plan and TODO documents, and perform the research required to gather the knowledge required to make good decisions and build good plans. + +The User will ask you to analyze documents and code, hunt for defects/bugs, recommend improvements, create documentation, and related tasks. You respond by answering their questions, performing the requested research and analysis, writing text responses, and writing or updating documents in the project directories as needed, etc. + +Prefer doing your own research in the code over asking the user basic/starter questions. Delegate research tasks to the Explore subagent, let it go learn about the project details that you want to know, and submit a report back to you. You can spawn more than one subagent at a time to explore multiple topics simultaneously. The subagent(s) will then use their tools to explore the project and report back to you as requested. This is better than asking the User about the project, and gives you a chance to detect problems that should to be fixed. + +While in Plan mode, you're NOT actively writing source code, making changes to code, or making changes to features. You are doing work, you will make git commits in Plan mode, but you're not actively working on features. You're describing the work to be done next together with the User in chat, creating and updating documents, splitting large tasks into workable phases and steps, and delegating tasks and work to subagents. + +## TOOL USAGE + +Use your tools to research and gather information. Read code, search for patterns, and explore the codebase to understand context before asking questions. Use subagents to delegate research tasks. Do not announce tool usage, just execute and use findings to inform your planning. + +You must remain within the project directory, which is the current working directory. You cannot access files and data outside of the project directory. You WILL NOT author scripts to work around the limitations of your tools. If you genuinely need something from outside the current working directory, ask the User to provide it for you, or for guidance on an alternate approach. Don't get stuck in a loop trying to figure out how to get something you can't have - ask the User to provide it (or an alternate approach) and stop. + +**DO NOT** spawn a subagent as a workaround for lacking tool features, tool failures, and tool errors. Instead, please respond by writing out what you were trying to do, the parameters you used when calling the tool, and the full response you received. Don't work around tool errors. Report faulty tool performance and behavior, then stop. Let the User help determine what to do next. + +## INSTRUCTIONS + +When the user sends you a prompt: + +1. Reason about what they're asking for (or asking you to do). +2. Plan out what you're going to do, the actions you're going to take, to satisfy the request and resolve the user's needs. + + a. Use the `ask_questions` tool to present the user with questions and up to 3 answers they can choose from (they will have a 4th: Type your own answer, provided by the system). + +3. Use your tools to investigate the code base, gain the knowledge needed about existing patterns in the code, etc., and formulate the actions you'll take to implement a solution to the User's problem or need. +4. Present your findings to the user and offer to update the plan document. +5. Create or update the plan documents (designs, plans, TODO lists, etc.) as needed. +6. Repeat until the User accepts the plan and switches to Build mode. + +You always end a turn by summarizing what you did to the User for their review and convenience. + +{{subagent_section}} + +## CONSTRAINTS + +- DO NOT work on code, edit code, create new code - those are Build mode tasks. +- DO NOT add/remove dependencies or make changes to the project files while in Plan mode - those are Build mode tasks. + +NOTE: DO NOT end a turn by using the ask_questions tool to ask, "Are you ready to switch to build mode?" - the User can't switch modes while using the questions tool. If you want to ask this question, write it in your response and let the turn end (stop). diff --git a/gadget-code/data/prompts/agent/ship/system.md b/gadget-code/data/prompts/agent/ship/system.md new file mode 100644 index 0000000..66b9a61 --- /dev/null +++ b/gadget-code/data/prompts/agent/ship/system.md @@ -0,0 +1,40 @@ +## ROLE + +You are Gadget. You are an AI software agent working as a software developer on a team. + +## SCOPE + +{{scope_block}} + +We are currently in Ship mode, which means you are preparing code for deployment, running final tests, creating releases, and ensuring everything is ready for production. + +## TOOL USAGE + +Use your tools decisively. Run tests, check builds, verify deployments, and execute release commands. Do not announce tool usage or ask permission. Just execute and report results. + +You must remain within the project directory, which is the current working directory. You cannot access files and data outside of the project directory. You WILL NOT author scripts to work around the limitations of your tools. If you genuinely need something from outside the current working directory, ask the User to provide it for you, or for guidance on an alternate approach. Don't get stuck in a loop trying to figure out how to get something you can't have - ask the User to provide it (or an alternate approach) and stop. + +**DO NOT** spawn a subagent as a workaround for lacking tool features, tool failures, and tool errors. Instead, please respond by writing out what you were trying to do, the parameters you used when calling the tool, and the full response you received. Don't work around tool errors. Report faulty tool performance and behavior, then stop. Let the User help determine what to do next. + +## INSTRUCTIONS + +When the user sends you a prompt: + +1. Reason about what they're asking for (or asking you to do). +2. Plan out what you're going to do to prepare for shipping. +3. Execute the plan: run tests, verify builds, create releases, deploy to production. +4. Report results and any issues encountered. + +You always end a turn by summarizing what you did to the User for their review and convenience. + +{{subagent_section}} + +## EXPECTED OUTCOMES + +In Ship mode, you will: + +1. Run test suites and verify all tests pass. +2. Build production artifacts. +3. Create git tags and releases. +4. Deploy to staging or production environments. +5. Verify deployments and monitor for issues. diff --git a/gadget-code/data/prompts/agent/test/system.md b/gadget-code/data/prompts/agent/test/system.md new file mode 100644 index 0000000..d5f4a8d --- /dev/null +++ b/gadget-code/data/prompts/agent/test/system.md @@ -0,0 +1,40 @@ +## ROLE + +You are Gadget. You are an AI software agent working as a software developer on a team. + +## SCOPE + +{{scope_block}} + +We are currently in Test mode, which means you are focused on writing tests, running test suites, debugging failing tests, and ensuring code quality through comprehensive testing. + +## TOOL USAGE + +Use your tools aggressively for testing. Run tests frequently, read test files, search for test patterns, and execute test commands. Do not announce tool usage or ask permission. Just run the tests and report results. + +You must remain within the project directory, which is the current working directory. You cannot access files and data outside of the project directory. You WILL NOT author scripts to work around the limitations of your tools. If you genuinely need something from outside the current working directory, ask the User to provide it for you, or for guidance on an alternate approach. Don't get stuck in a loop trying to figure out how to get something you can't have - ask the User to provide it (or an alternate approach) and stop. + +**DO NOT** spawn a subagent as a workaround for lacking tool features, tool failures, and tool errors. Instead, please respond by writing out what you were trying to do, the parameters you used when calling the tool, and the full response you received. Don't work around tool errors. Report faulty tool performance and behavior, then stop. Let the User help determine what to do next. + +## INSTRUCTIONS + +When the user sends you a prompt: + +1. Reason about what they're asking for (or asking you to do). +2. Plan out your testing strategy. +3. Execute tests, write new tests, fix failing tests, or debug test issues. +4. Report test results and any failures with details. + +You always end a turn by summarizing what you did to the User for their review and convenience. + +{{subagent_section}} + +## EXPECTED OUTCOMES + +In Test mode, you will: + +1. Write unit, integration, and end-to-end tests. +2. Run test suites and analyze results. +3. Debug failing tests and fix test issues. +4. Ensure code coverage meets requirements. +5. Validate that new features work as expected through testing. diff --git a/gadget-code/data/prompts/common/scope-block.md b/gadget-code/data/prompts/common/scope-block.md new file mode 100644 index 0000000..de48f25 --- /dev/null +++ b/gadget-code/data/prompts/common/scope-block.md @@ -0,0 +1,9 @@ +Your current working directory is a project that the User is working on with you. You can explore anything in this directory except for secrets and credentials. You cannot explore outside of this directory. If you execute shell commands, you do so only to learn (not to make changes). + +The Gadget Code system operates in one of five modes: + +- Plan: For planning out what needs to be done. +- Build: For doing what needs to be done. +- Test: For testing what got done. +- Ship: For deploying or "shipping" what got tested to be correct. +- Develop: A special mode for working on the harness itself (Gadget Code, you). diff --git a/gadget-code/data/prompts/common/subagents.md b/gadget-code/data/prompts/common/subagents.md new file mode 100644 index 0000000..3d87bf7 --- /dev/null +++ b/gadget-code/data/prompts/common/subagents.md @@ -0,0 +1,30 @@ +## SUBAGENTS + +There are two subagents you can spawn to perform work and save you memory and effort in the session context with the user. Subagents are useful when you know there will be many tool calls involved in an operation. + +The subagents will: + +1. Make many tool calls and pollute their own context with all the extraneous information you don't and won't need +2. Perform the work requested, and filter the information in the response down to what you **do** need +3. Respond to you with the information or result you need in order to continue working on your tasks. + +Always be specific when asking a subagent to do work. When constructing your prompt for the subagent: + +1. Tell the agent where to begin (if relevant) +2. Tell the agent exactly what to do or what to find +3. Give the agent the information YOU HAVE that will help it do it's work +4. Don't give the agent confusing or misleading directions +5. Define "done" for the agent so it knows when to stop working +6. Tell the agent exactly how to report back to you when done + +### SUBAGENT: EXPLORE + +The Explore subagent is tuned for knowing how to dig through a project directory, find the requested information, and generate a report back to you containing the information you need (if available). + +Use this subagent to learn about a project's structure, architecture, or actual content such as program source code, documentation, etc. + +### SUBAGENT: GENERAL + +The General subagent can perform finite-scoped and well-defined tasks for you, calling many tools to get work done. It will report back to you with your requested result or status. This saves you from polluting your own session context with all the sub-steps and actions taken, etc., and lets you know that work got done so you can cross tasks off your list without cluttering your thoughts going forward. + +Use the general subagent to have work done, write software and tests, write documentation, and perform project maintenance chores on your behalf. Provide it with clear instructions for what to do, how to do it, and how to respond to you to get work done and the response you need. diff --git a/gadget-code/data/prompts/subagent/explore/system.md b/gadget-code/data/prompts/subagent/explore/system.md new file mode 100644 index 0000000..a8d7529 --- /dev/null +++ b/gadget-code/data/prompts/subagent/explore/system.md @@ -0,0 +1,35 @@ +## ROLE + +You are the read-only Explore subagent in Gadget Code. You research and explore code bases and collections of information to look for knowledge that is needed by other agents to do their work. + +## SCOPE + +You are spawned as a child process by the Master Control Console (MCC) in Gadget Code to find specific information and respond with a specific response to provide that information back to the MCC. You follow your instructions and use your tools to find and report the requested information. + +You will generally be tasked with exploring a software repository to learn about the software architecture, how things are imlemented, the structure of a class and it's methods, the order of operations as they are performed by the software, or even just read and summarize documentation looking for a specific piece of knowledge for the MCC. + +{{tool_aggression}} + +This is your job. + +## TOOL USAGE + +Use your tools immediately and aggressively. Do not announce tool usage, ask permission, or describe what you're about to do. Just execute. When you need to check a file, read it. When you need to find something, search for it. When you need to verify something, run the command. Explain your reasoning and results after taking action, not before. + +You must remain within the project directory, which is the current working directory. You cannot access files and data outside of the project directory. You WILL NOT author scripts to work around the limitations of your tools. Don't get stuck in a loop trying to figure out how to get something you can't have - say that you were unable to proceed, and stop. + +**DO NOT** attempt workarounds for lacking tool features, tool failures, and tool errors. Instead, please respond by writing out what you were trying to do, the parameters you used when calling the tool, and the full response you received. Don't work around tool errors. Report faulty tool performance and behavior, then stop. + +## INSTRUCTIONS + +The Master Control Console (MCC) will create an instance of you and prompt you with specific instructions for a task you are to complete. + +1. Reason about the request and figure out what it is that you'll need to do. +2. Perform the work requested by the MCC. +3. Respond with the information requested by the MCC. + +Keep your output as brief as possible. You are not communicating with a human. You are communicating with a software agent. Communicate the knowledge requested back to the MCC in your response using clear and concise language intended to calibrate the agent for success while doing it's work. + +## CONSTRAINTS + +- DO NOT make any changes to any files under any circumstances - you are a read-only subagent. diff --git a/gadget-code/data/prompts/subagent/general/system.md b/gadget-code/data/prompts/subagent/general/system.md new file mode 100644 index 0000000..c15b0ba --- /dev/null +++ b/gadget-code/data/prompts/subagent/general/system.md @@ -0,0 +1,40 @@ +## ROLE + +You are the General subagent in Gadget Code. You work in code bases and collections of information to perform general tasks. + +## SCOPE + +You are spawned as a child process by the Master Control Console (MCC) in Gadget Code to perform a specific task and respond with a specific response. You follow your instructions and use your tools to accomplish the work requested by the MCC. This is your job. + +You will generally be tasked with exploring a software repository to learn about the software architecture, how things are imlemented, the structure of a class and it's methods, or even just read and summarize documentation looking for a specific piece of knowledge for the MCC. + +{{tool_aggression}} + +This is your job. + +## TOOL USAGE + +Use your tools immediately and aggressively. Do not announce tool usage, ask permission, or describe what you're about to do. Just execute. When you need to check a file, read it. When you need to find something, search for it. When you need to verify something, run the command. Explain your reasoning and results after taking action, not before. + +You must remain within the project directory, which is the current working directory. You cannot access files and data outside of the project directory. You WILL NOT author scripts to work around the limitations of your tools. If you genuinely need something from outside the current working directory, ask the User to provide it for you, or for guidance on an alternate approach. Don't get stuck in a loop trying to figure out how to get something you can't have - ask the User to provide it (or an alternate approach) and stop. + +**DO NOT** spawn a subagent as a workaround for lacking tool features, tool failures, and tool errors. Instead, please respond by writing out what you were trying to do, the parameters you used when calling the tool, and the full response you received. Don't work around tool errors. Report faulty tool performance and behavior, then stop. Let the User help determine what to do next. + +## INSTRUCTIONS + +The Master Control Console (MCC) will create an instance of you and prompt you with specific instructions for a task you are to complete. + +1. Reason about the request and figure out what it is that you'll need to do. +2. Use your tools to perform the work requested by the MCC. +3. Respond with the information requested by the MCC. + +Keep your output as brief as possible. You are not communicating with a human. You are communicating with a software agent. Communicate the knowledge requested back to the MCC in your response using clear and concise language intended to calibrate the agent for success while doing it's work. + +You are not permitted to access resources outside of the project working directory and it's subdirectories. If you need something from outside the current working directory, your tools will not be able to provide it. Ask the MCC to provide whatever you need from outside of the project for you. Or, ask the MCC for guidance on an alternate approach, and stop. The MCC will spawn a new subagent with the information needed, or a different approach. + +## CONSTRAINTS + +- You must remain within the project directory, which is the current working directory. +- You cannot access files and data outside of the project directory. +- You WILL NOT author scripts to work around the limitations of your tools. +- Don't get stuck in a loop trying to figure out how to get something you can't have - ask the User to provide it (or an alternate approach) and stop. diff --git a/gadget-code/deploy b/gadget-code/deploy new file mode 100755 index 0000000..139973a --- /dev/null +++ b/gadget-code/deploy @@ -0,0 +1,25 @@ +#!/bin/bash + +PRODUCT="gadget-code" +PRODUCTION_BRANCH="master" + +echo "-------------------------------------------------------------------------" +echo "Updating codebase..." + +git checkout $PRODUCTION_BRANCH +git pull origin $PRODUCTION_BRANCH +pnpm i + +echo "-------------------------------------------------------------------------" +echo "Building application server..." + +./build + +echo "-------------------------------------------------------------------------" +echo "Restarting application server..." + +sudo supervisorctl stop "${PRODUCT}-web:*" +sudo supervisorctl start "${PRODUCT}-web:*" + +echo "-------------------------------------------------------------------------" +echo "Done." diff --git a/gadget-code/docs/agentic-workflow-loop.md b/gadget-code/docs/agentic-workflow-loop.md new file mode 100644 index 0000000..712e96a --- /dev/null +++ b/gadget-code/docs/agentic-workflow-loop.md @@ -0,0 +1,134 @@ +# The Agentic Workflow Loop + +Gadget Code is a software engineering Integrated Development Environment (IDE) built around an Agentic Workflow Loop (AWL). The User can edit project files by hand, and also ask Gadget to do work for them in the project. + +A technical chat interface is offered as the Chat Session view where the User can send a prompt to Gadget for processing. The prompt is packaged as a Work Order, and sent to an available Gadget Drone for processing. The Agentic Workflow Loop resides and executes within the Gadget Drone. + +The browser is used as a remote control of sorts for Agentic Workflow Loops running in the User's drones on the user's behalf. As the agent is performing work, it emits messages back to the browser (using Socket.IO by way of the web server) to update the User's display in the Chat Session view. + +The loop starts when the User submits a prompt for processing, and ends when an AI API call responds with zero tool calls. If a response contains tool calls, the loop calls the tools, inserts "tool" role response messages in the context, and remains in the loop to submit the context back to the model for additional thinking, response, and tool call responses. + +## The Work Order + +Each Gadget Drone registered by the User implements a named Bull job queue. Gadget Code will create a Work Order job for the Drone to process. + +The Work Order is a JSON object that contains information about the project, AI service provider and model, and also the User's prompt. + +````typescript +/* + * Chat session context is made of chat messages with a timestamp and role. The + * role is "system", "user", "assistant", or "tool". The assistant can have + * reasoning or "thinking" content. When thinking content is being included, we + * merge the thinking + response by enclosing the thinking content in a + * element in the content. + * + * ``` + * I need to research the project... + * I found the bug! + * ``` + */ +interface IChatMessage { + createdAt: Date; + role: string; + content: string; +} + +interface IWorkOrder { + project: { + _id: string; + name: string; + slug: string; + gitUrl: string; + }; + session: { + name: string; + mode: string; + }; + provider: { + baseUrl: string; + apiKey: string; + modelId: string; + params: { + temperature: number; + topP: number; + topK: number; + }; + }; + context: IChatMessage[]; + prompt: string; +} +```` + +## The Workflow Loop + +When a Work Order job is received a Gadget Drone, it performs a series of steps in preparation to work on the prompt contained in the work order. + +1. Check if the workspace directory currently has the project cloned. If not, clone the project from git into the workspace directory. +1. Instantiate the correct AI API client object configured with the credentials provided in the work order. + +```typescript +/* + * This is just pseudocode that shows the intent. It does not account for every + * aspect of the loop. + */ +async function workflowLoop( + session: IChatSession, + provider: IAiProvider, + llm: string, + userPrompt: string, +): Promise { + const NOW = new Date(); + + /* + * Create the ChatTurn in the database at the *start* of the turn, and update + * it with information as the turn executes. This helps ensure we don't lose + * work conducted in a turn if an error happens. A turn should be able to be + * resumed whenever possible. + */ + + const turn = new ChatTurn(); + turn.createdAt = NOW; + turn.status = ChatTurnStatus.Processing; + + turn.user = session.user; + turn.provider = provider; + turn.llm = llm; + turn.mode = session.mode; + turn.prompt = userPrompt; + + turn.stats = { + toolCallCount: 0, + inputTokens: 0, + thinkintTokenCount: 0, + responseTokens: 0, + durationMs: 0, + durationLabel: "pending", + }; + + await turn.save(); + + /* emit Socket.IO message with turn + + // this turn's context (system, history, prompt, work) + const messages = []; + + const systemPrompt = buildSystemPrompt(session); + messages.push({ role: "system", content: systemPrompt }); + + // recall full session history into messages array + buildSessionContext(session, messages); + + // push the User's latest prompt to the context + messages.push({ role: "user", content: userPrompt }); + + let keepProcessing = true; + do { + const response = await aiApiCall(messages); + keeProcessing = response.tool_calls.length > 0; + for (const tool_call of response.tool_calls) { + const response = await callTool(tool_call.name, tool_call.args); + messages.push({ role: "tool", content: response }); + } + } while (keepProcessing); +} +``` diff --git a/gadget-code/docs/chat-session.md b/gadget-code/docs/chat-session.md new file mode 100644 index 0000000..e69de29 diff --git a/gadget-code/docs/gadget-drone.md b/gadget-code/docs/gadget-drone.md new file mode 100644 index 0000000..a7f8d7d --- /dev/null +++ b/gadget-code/docs/gadget-drone.md @@ -0,0 +1,13 @@ +# Gadget Drone + +Gadget Drone is a component in the Gadget Code ecosystem that operates as a headless/non-interactive process on a host in a directory on storage (once authenticated). Gadget Drone: + +1. Assumes ownership of process.cwd() on startup, and verifies that the directory is either already a Gadget Drone directory, or empty. If not a Gadget Drone directory, and if also not empty, Gadget Drone refuses to start with a message printed to the console, and terminates. + +2. Creates the .gadget and .gadget-cache directories in the directory on launch if they are not there. + +## .gadget directory + +When the Gadget Drone starts and initializes, + +## .gadget-cache directory diff --git a/gadget-code/docs/project.md b/gadget-code/docs/project.md new file mode 100644 index 0000000..7712cdf --- /dev/null +++ b/gadget-code/docs/project.md @@ -0,0 +1,29 @@ +# Gadget Code: Projects + +The User will create one or more Project records for tracking a software development project in Gadget Code. + +It will be uncommon to query this model/collection without a User. System administrators and managers will have interest in the global Projects list. Developers will not. A Developer only cares about their own projects. + +It's not a security violation for a User to see another User's projects. It's just superflous information. John doesn't care about Nancy's projects. John works on John's projects, and he owns them. + +## createdAt + +The date and time at which the User created the Project record in Gadget Code. This is not necessarily when the project itself was created or started. This is just when Gadget Code got involved. + +## user + +The User for whom the Project was created. The User is the owner of the project. Users can only see their own Projects. A user is thought of as working in their project. If multiple Users are in the chat, one of them is the Project Owner and will be marked as such. The other Users in the chat session are reffered to as Collaborators. + +## name + +The name of the project as displayed in lists, headers, title areas, etc. + +## slug + +The project's slug (my-project), which is used as it's directory name in the workspace directory by [Gadget Drone](./gadget-drone.md). + +## gitUrl + +The URL of the project's git repository (optional). This will be used to clone the project into the workspace directory on the drones. If this is not set, a new git repo will be created in the workspace directory and the url will be stored here. + +If a gitUrl is not set for a Project and the User selects to delete the project, the project directory will be removed, and it will NOT be pushed to git. The User should be informed that the contents of the directory will be lost, and should have to confirm that this is what they want to do. diff --git a/gadget-code/frontend/index.html b/gadget-code/frontend/index.html new file mode 100644 index 0000000..52aff94 --- /dev/null +++ b/gadget-code/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Gadget Code + + +
+ + + diff --git a/gadget-code/frontend/package.json b/gadget-code/frontend/package.json new file mode 100644 index 0000000..c32516a --- /dev/null +++ b/gadget-code/frontend/package.json @@ -0,0 +1,11 @@ +{ + "name": "gadget-code-frontend", + "version": "1.0.0", + "description": "Gadget Code Frontend - A self-hosted Agentic Engineering Platform (AEP).", + "type": "module", + "scripts": { + "build": "vite build" + }, + "author": "Robert Colbert ", + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/gadget-code/frontend/postcss.config.js b/gadget-code/frontend/postcss.config.js new file mode 100644 index 0000000..27ae1c9 --- /dev/null +++ b/gadget-code/frontend/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; \ No newline at end of file diff --git a/gadget-code/frontend/src/App.tsx b/gadget-code/frontend/src/App.tsx new file mode 100644 index 0000000..59962da --- /dev/null +++ b/gadget-code/frontend/src/App.tsx @@ -0,0 +1,96 @@ +import { useState, useEffect } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { User } from './lib/api'; +import { socketClient } from './lib/socket'; +import Navbar from './components/Navbar'; +import Home from './pages/Home'; +import SignIn from './pages/SignIn'; +import SignUp from './pages/SignUp'; + +const TOKEN_KEY = 'dtp_auth_token'; +const USER_KEY = 'dtp_user'; + +function getStoredUser(): User | null { + try { + const userData = localStorage.getItem(USER_KEY); + return userData ? JSON.parse(userData) : null; + } catch { + return null; + } +} + +function setStoredUser(user: User | null) { + if (user) { + localStorage.setItem(USER_KEY, JSON.stringify(user)); + } else { + localStorage.removeItem(USER_KEY); + } +} + +export default function App() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const storedUser = getStoredUser(); + const storedToken = localStorage.getItem(TOKEN_KEY); + if (storedUser && storedToken) { + setUser(storedUser); + socketClient.connect(storedToken); + } + setLoading(false); + }, []); + + const handleSignInSuccess = (newUser: User, token: string) => { + localStorage.setItem(TOKEN_KEY, token); + setStoredUser(newUser); + setUser(newUser); + socketClient.connect(token); + }; + + const handleSignOut = () => { + localStorage.removeItem(TOKEN_KEY); + setStoredUser(null); + setUser(null); + socketClient.disconnect(); + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( + +
+ + + } /> + + ) : ( + + ) + } + /> + + ) : ( + + ) + } + /> + +
+
+ ); +} \ No newline at end of file diff --git a/gadget-code/frontend/src/components/Navbar.tsx b/gadget-code/frontend/src/components/Navbar.tsx new file mode 100644 index 0000000..ed2707c --- /dev/null +++ b/gadget-code/frontend/src/components/Navbar.tsx @@ -0,0 +1,52 @@ +import { Link } from 'react-router-dom'; + +interface User { + _id: string; + email: string; + displayName: string; +} + +interface NavbarProps { + user: User | null; + onSignOut: () => void; +} + +export default function Navbar({ user, onSignOut }: NavbarProps) { + return ( + + ); +} \ No newline at end of file diff --git a/gadget-code/frontend/src/index.css b/gadget-code/frontend/src/index.css new file mode 100644 index 0000000..d52b3a5 --- /dev/null +++ b/gadget-code/frontend/src/index.css @@ -0,0 +1,29 @@ +@import "tailwindcss"; + +:root { + --color-bg: #0f0f12; + --color-bg-secondary: #1a1a1f; + --color-text: #e4e4e7; + --color-text-muted: #a1a1aa; + --color-primary: #3b82f6; + --color-primary-hover: #2563eb; + --color-border: #27272a; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background-color: var(--color-bg); + color: var(--color-text); + line-height: 1.6; +} + +#root { + min-height: 100vh; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/gadget-code/frontend/src/lib/api.ts b/gadget-code/frontend/src/lib/api.ts new file mode 100644 index 0000000..3cfb3aa --- /dev/null +++ b/gadget-code/frontend/src/lib/api.ts @@ -0,0 +1,64 @@ +const API_BASE = ''; + +export interface ApiResponse { + success: boolean; + message?: string; + user?: T; + token?: string; + data?: T; +} + +async function request( + method: string, + path: string, + body?: Record +): Promise { + const options: RequestInit = { + method, + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(`${API_BASE}${path}`, options); + const json = await response.json() as ApiResponse; + + if (!json.success) { + throw new Error(json.message || 'Request failed'); + } + + // For auth endpoints that return {success, user, token}, return the full response + if (json.token !== undefined && json.user !== undefined) { + return json as T; + } + if (json.data !== undefined) { + return json.data as T; + } + return json as T; +} + +export const api = { + get: (path: string) => request('GET', path), + post: (path: string, body: Record) => + request('POST', path, body), + put: (path: string, body: Record) => + request('PUT', path, body), + delete: (path: string) => request('DELETE', path), +}; + +export interface User { + _id: string; + email: string; + displayName: string; + flags: string[]; +} + +export interface AuthResponse { + user: User; + token: string; +} \ No newline at end of file diff --git a/gadget-code/frontend/src/lib/socket.ts b/gadget-code/frontend/src/lib/socket.ts new file mode 100644 index 0000000..0de3080 --- /dev/null +++ b/gadget-code/frontend/src/lib/socket.ts @@ -0,0 +1,98 @@ +import { io, Socket } from 'socket.io-client'; + +const SOCKET_URL = ''; + +export interface SocketEvents { + 'agent:thinking': (data: { agentId: string; thinking: string }) => void; + 'agent:response': (data: { agentId: string; chunk: string }) => void; + 'agent:tool-call': (data: { agentId: string; tool: string; args: unknown }) => void; + 'agent:tool-result': (data: { agentId: string; tool: string; result: unknown }) => void; + 'agent:complete': (data: { agentId: string }) => void; + 'log:entry': (data: { level: string; message: string; timestamp: number }) => void; + 'chat:message': (data: { agentId: string; message: string; role: 'user' | 'assistant' | 'system' }) => void; + connect: () => void; + disconnect: () => void; + error: (error: Error) => void; +} + +class SocketClient { + private socket: Socket | null = null; + private eventListeners: Map void>> = new Map(); + private reconnectAttempts = 0; + private maxReconnectAttempts = 5; + private jwt: string | null = null; + + get connected(): boolean { + return this.socket?.connected ?? false; + } + + connect(token: string): void { + if (this.socket?.connected) { + return; + } + + this.jwt = token; + + this.socket = io(SOCKET_URL, { + auth: { + token: this.jwt, + }, + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionAttempts: this.maxReconnectAttempts, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + }); + + this.socket.on('connect', () => { + this.reconnectAttempts = 0; + this.emit('connect'); + }); + + this.socket.on('disconnect', (reason) => { + this.emit('disconnect', reason); + }); + + this.socket.on('connect_error', (error) => { + this.reconnectAttempts++; + this.emit('error', error); + }); + } + + disconnect(): void { + if (this.socket) { + this.socket.disconnect(); + this.socket = null; + this.jwt = null; + } + } + + on(event: K, callback: SocketEvents[K]): void { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, new Set()); + } + this.eventListeners.get(event)!.add(callback as (...args: unknown[]) => void); + } + + off(event: K, callback: SocketEvents[K]): void { + const listeners = this.eventListeners.get(event); + if (listeners) { + listeners.delete(callback as (...args: unknown[]) => void); + } + } + + emit(event: K, ...args: Parameters): void { + const listeners = this.eventListeners.get(event); + if (listeners) { + listeners.forEach((callback) => callback(...args)); + } + } + + send(event: K, ...args: Parameters): void { + if (this.socket?.connected) { + this.socket.emit(event, ...args); + } + } +} + +export const socketClient = new SocketClient(); \ No newline at end of file diff --git a/gadget-code/frontend/src/main.tsx b/gadget-code/frontend/src/main.tsx new file mode 100644 index 0000000..b3c11a7 --- /dev/null +++ b/gadget-code/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); \ No newline at end of file diff --git a/gadget-code/frontend/src/pages/Home.tsx b/gadget-code/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..77a85a6 --- /dev/null +++ b/gadget-code/frontend/src/pages/Home.tsx @@ -0,0 +1,48 @@ +import { useState, useEffect } from "react"; +import { api, User } from "../lib/api"; + +interface HomeProps { + user: User | null; +} + +export default function Home({ user }: HomeProps) { + if (!user) { + return ( +
+
+

Gadget Code

+

+ Agentic Engineering IDE +

+ +
+
+ ); + } + + return ( +
+
+

+ Welcome, {user.displayName}! +

+

+ And then this is where you write some code to build an app. +

+
+
+ ); +} diff --git a/gadget-code/frontend/src/pages/SignIn.tsx b/gadget-code/frontend/src/pages/SignIn.tsx new file mode 100644 index 0000000..ab2cea5 --- /dev/null +++ b/gadget-code/frontend/src/pages/SignIn.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api, AuthResponse, User } from '../lib/api'; + +interface SignInProps { + onSuccess: (user: User, token: string) => void; +} + +export default function SignIn({ onSuccess }: SignInProps) { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + try { + const response = await api.post('/auth/sign-in', { email, password }); + onSuccess(response.user, response.token); + } catch (err) { + setError(err instanceof Error ? err.message : 'Invalid credentials'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Sign In

+ {error && ( +
+ {error} +
+ )} +
+
+ + setEmail(e.target.value)} + className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" + required + /> +
+
+ + setPassword(e.target.value)} + className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" + required + /> +
+
+ + Cancel + + +
+
+

+ Don't have an account?{' '} + + Sign up + +

+
+
+ ); +} \ No newline at end of file diff --git a/gadget-code/frontend/src/pages/SignUp.tsx b/gadget-code/frontend/src/pages/SignUp.tsx new file mode 100644 index 0000000..48c7199 --- /dev/null +++ b/gadget-code/frontend/src/pages/SignUp.tsx @@ -0,0 +1,122 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api, AuthResponse, User } from '../lib/api'; + +interface SignUpProps { + onSuccess: (user: User, token: string) => void; +} + +export default function SignUp({ onSuccess }: SignUpProps) { + const [email, setEmail] = useState(''); + const [displayName, setDisplayName] = useState(''); + const [password, setPassword] = useState(''); + const [passwordVerify, setPasswordVerify] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (password !== passwordVerify) { + setError('Passwords do not match'); + return; + } + + setLoading(true); + try { + const response = await api.post('/auth/sign-up', { + email, + password, + displayName, + }); + onSuccess(response.user, response.token); + } catch (err) { + setError(err instanceof Error ? err.message : 'Sign up failed'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Create Account

+ {error && ( +
+ {error} +
+ )} +
+
+ + setEmail(e.target.value)} + className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" + required + /> +
+
+ + setDisplayName(e.target.value)} + className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" + required + /> +
+
+ + setPassword(e.target.value)} + className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" + required + /> +
+
+ + setPasswordVerify(e.target.value)} + className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" + required + /> +
+
+ + Cancel + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/gadget-code/frontend/tailwind.config.js b/gadget-code/frontend/tailwind.config.js new file mode 100644 index 0000000..f07a3e8 --- /dev/null +++ b/gadget-code/frontend/tailwind.config.js @@ -0,0 +1,19 @@ +import type { Config } from 'tailwindcss'; + +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + bg: '#0f0f12', + 'bg-secondary': '#1a1a1f', + text: '#e4e4e7', + 'text-muted': '#a1a1aa', + primary: '#3b82f6', + 'primary-hover': '#2563eb', + border: '#27272a', + }, + }, + }, + plugins: [], +} satisfies Config; \ No newline at end of file diff --git a/gadget-code/frontend/vite.config.ts b/gadget-code/frontend/vite.config.ts new file mode 100644 index 0000000..c12ddab --- /dev/null +++ b/gadget-code/frontend/vite.config.ts @@ -0,0 +1,56 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, '..'); + +export default defineConfig({ + plugins: [react()], + root: '.', + publicDir: 'public', + server: { + port: 5174, + host: '0.0.0.0', + https: { + key: fs.readFileSync(path.join(rootDir, 'ssl', 'code-dev.g4dge7.com.key')), + cert: fs.readFileSync(path.join(rootDir, 'ssl', 'code-dev.g4dge7.com.crt')), + }, + hmr: { + host: 'code-dev.g4dge7.com', + port: 5174, + protocol: 'wss', + }, + proxy: { + '/auth': { + target: 'http://localhost:3443', + changeOrigin: true, + cookieDomainRewrite: { + '': 'code-dev.g4dge7.com', + }, + }, + '/api': { + target: 'http://localhost:3443', + changeOrigin: true, + cookieDomainRewrite: { + '': 'code-dev.g4dge7.com', + }, + }, + '/socket.io': { + target: 'http://localhost:3443', + ws: true, + }, + }, + }, + build: { + outDir: path.join(rootDir, 'dist', 'client'), + emptyOutDir: true, + }, + resolve: { + alias: { + '@': path.join(rootDir, 'src'), + }, + }, +}); \ No newline at end of file diff --git a/gadget-code/gadget-code.code-workspace b/gadget-code/gadget-code.code-workspace new file mode 100644 index 0000000..e529cc2 --- /dev/null +++ b/gadget-code/gadget-code.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": ".", + }, + ], + "settings": {}, +} diff --git a/gadget-code/nginx/webapp.digitaltelepresence.com b/gadget-code/nginx/webapp.digitaltelepresence.com new file mode 100644 index 0000000..d05adbd --- /dev/null +++ b/gadget-code/nginx/webapp.digitaltelepresence.com @@ -0,0 +1,44 @@ +upstream webapp { + least_conn; + server 127.0.0.1:3600; + server 127.0.0.1:3601; +} + +server { + server_name webapp.digitaltelepresence.com + + location /assets { + root /home/dtp/live/dtp-webapp; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + + proxy_pass http://webapp; + proxy_max_temp_file_size 0; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + listen 443 ssl; + listen [::]:443 ssl ipv6only=on; + + ssl_certificate /home/dtp/keys/cloudflare-origin.pem + ssl_certificate_key /home/dtp/keys/cloudflare-origin.key +} + +server { + server_name webapp.digitaltelepresence.com; + + if ($host = webapp.digitaltelepresence.com) { + return 301 https://$host$request_uri; + } + + listen 80; + listen [::]:80; + + return 404; +} diff --git a/gadget-code/package.json b/gadget-code/package.json new file mode 100644 index 0000000..081ea1e --- /dev/null +++ b/gadget-code/package.json @@ -0,0 +1,101 @@ +{ + "name": "gadget-code", + "version": "1.0.0", + "description": "Gadget Code - A self-hosted Agentic Engineering Platform (AEP).", + "type": "module", + "main": "index.js", + "scripts": { + "build": "pnpm build:backend && pnpm build:frontend", + "build:backend": "pnpm tsc && pnpm tsc-alias", + "build:frontend": "cd frontend && pnpm build", + "dev": "tsx ./src/web-app.ts", + "dev:backend": "tsx ./src/web-app.ts", + "dev:backend:watch": "tsx watch ./src/web-app.ts", + "dev:frontend": "cd frontend && vite", + "cli": "NODE_ENV=production node ./dist/web-cli.js", + "start": "NODE_ENV=production node ./dist/web-app.js", + "test": "vitest" + }, + "keywords": [], + "author": "Robert Colbert ", + "license": "Apache-2.0", + "packageManager": "pnpm@10.12.3", + "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.2", + "@gadget/ai": "workspace:*", + "ansicolor": "^2.0.3", + "bull": "^4.16.5", + "chart.js": "^4.5.0", + "compression": "^1.8.0", + "connect-redis": "^8.0.2", + "cookie-parser": "^1.4.7", + "cron": "^4.3.1", + "dayjs": "^1.11.13", + "dotenv": "^16.6.0", + "dtp-cleantext": "^1.0.0", + "express": "^5.1.0", + "express-rate-limit": "^7.5.1", + "express-session": "^1.18.1", + "geoip-lite": "^1.4.10", + "has-flag": "^5.0.1", + "ioredis": "^5.6.0", + "jsonwebtoken": "^9.0.2", + "marked": "^16.0.0", + "method-override": "^3.0.0", + "minio": "^8.0.5", + "mongoose": "^8.16.1", + "morgan": "^1.10.0", + "multer": "^2.0.1", + "nodemailer": "^7.0.3", + "numeral": "^2.0.6", + "pug": "^3.0.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-router-dom": "^7.14.2", + "rotating-file-stream": "^3.2.6", + "serve-favicon": "^2.5.1", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", + "uikit": "^3.23.11", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@playwright/test": "^1.59.1", + "@tailwindcss/postcss": "^4.2.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/browser-sync": "^2.29.0", + "@types/compression": "^1.8.1", + "@types/cookie-parser": "^1.4.9", + "@types/express": "^5.0.3", + "@types/express-session": "^1.18.2", + "@types/geoip-lite": "^1.4.4", + "@types/jsonwebtoken": "^9.0.10", + "@types/less": "^3.0.8", + "@types/method-override": "^3.0.0", + "@types/morgan": "^1.9.10", + "@types/multer": "^1.4.13", + "@types/node": "^24.0.4", + "@types/nodemailer": "^6.4.17", + "@types/numeral": "^2.0.5", + "@types/react": "^19.2.14", + "@types/serve-favicon": "^2.5.7", + "@types/uikit": "^3.14.5", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "browser-sync": "^3.0.4", + "esbuild": "^0.25.5", + "globals": "^16.2.0", + "jsdom": "^29.0.2", + "less": "^4.3.0", + "postcss": "^8.5.10", + "tailwindcss": "^4.2.4", + "tsc-alias": "^1.0.7", + "tslib": "^2.8.1", + "tsx": "^4.19.2", + "typescript": "^5.8.3", + "vite": "^8.0.10", + "vitest": "^4.1.5" + } +} diff --git a/gadget-code/playwright.config.ts b/gadget-code/playwright.config.ts new file mode 100644 index 0000000..91ba603 --- /dev/null +++ b/gadget-code/playwright.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 60000, + use: { + baseURL: 'https://code-dev.g4dge7.com:5174', + ignoreHTTPSErrors: true, + headless: true, + }, +}); \ No newline at end of file diff --git a/gadget-code/pnpm-lock.yaml b/gadget-code/pnpm-lock.yaml new file mode 100644 index 0000000..c5db72f --- /dev/null +++ b/gadget-code/pnpm-lock.yaml @@ -0,0 +1,5981 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@fortawesome/fontawesome-free': + specifier: ^6.7.2 + version: 6.7.2 + ansicolor: + specifier: ^2.0.3 + version: 2.0.3 + bull: + specifier: ^4.16.5 + version: 4.16.5 + chart.js: + specifier: ^4.5.0 + version: 4.5.0 + compression: + specifier: ^1.8.0 + version: 1.8.0 + connect-redis: + specifier: ^8.0.2 + version: 8.0.2(express-session@1.18.1) + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + cron: + specifier: ^4.3.1 + version: 4.3.1 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + dotenv: + specifier: ^16.6.0 + version: 16.6.0 + dtp-cleantext: + specifier: ^1.0.0 + version: 1.0.0 + express: + specifier: ^5.1.0 + version: 5.1.0 + express-rate-limit: + specifier: ^7.5.1 + version: 7.5.1(express@5.1.0) + express-session: + specifier: ^1.18.1 + version: 1.18.1 + geoip-lite: + specifier: ^1.4.10 + version: 1.4.10 + has-flag: + specifier: ^5.0.1 + version: 5.0.1 + ioredis: + specifier: ^5.6.0 + version: 5.6.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + marked: + specifier: ^16.0.0 + version: 16.0.0 + method-override: + specifier: ^3.0.0 + version: 3.0.0 + minio: + specifier: ^8.0.5 + version: 8.0.5 + mongoose: + specifier: ^8.16.1 + version: 8.16.1 + morgan: + specifier: ^1.10.0 + version: 1.10.0 + multer: + specifier: ^2.0.1 + version: 2.0.1 + nodemailer: + specifier: ^7.0.3 + version: 7.0.3 + numeral: + specifier: ^2.0.6 + version: 2.0.6 + ollama: + specifier: ^0.6.3 + version: 0.6.3 + pug: + specifier: ^3.0.3 + version: 3.0.3 + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + react-router-dom: + specifier: ^7.14.2 + version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + rotating-file-stream: + specifier: ^3.2.6 + version: 3.2.6 + serve-favicon: + specifier: ^2.5.1 + version: 2.5.1 + socket.io: + specifier: ^4.8.3 + version: 4.8.3 + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + uikit: + specifier: ^3.23.11 + version: 3.23.11 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + devDependencies: + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 + '@tailwindcss/postcss': + specifier: ^4.2.4 + version: 4.2.4 + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/browser-sync': + specifier: ^2.29.0 + version: 2.29.0 + '@types/compression': + specifier: ^1.8.1 + version: 1.8.1 + '@types/cookie-parser': + specifier: ^1.4.9 + version: 1.4.9(@types/express@5.0.3) + '@types/express': + specifier: ^5.0.3 + version: 5.0.3 + '@types/express-session': + specifier: ^1.18.2 + version: 1.18.2 + '@types/geoip-lite': + specifier: ^1.4.4 + version: 1.4.4 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/less': + specifier: ^3.0.8 + version: 3.0.8 + '@types/method-override': + specifier: ^3.0.0 + version: 3.0.0(@types/express@5.0.3) + '@types/morgan': + specifier: ^1.9.10 + version: 1.9.10 + '@types/multer': + specifier: ^1.4.13 + version: 1.4.13 + '@types/node': + specifier: ^24.0.4 + version: 24.0.4 + '@types/nodemailer': + specifier: ^6.4.17 + version: 6.4.17 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 + '@types/serve-favicon': + specifier: ^2.5.7 + version: 2.5.7 + '@types/uikit': + specifier: ^3.14.5 + version: 3.14.5 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0)) + autoprefixer: + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.10) + browser-sync: + specifier: ^3.0.4 + version: 3.0.4 + esbuild: + specifier: ^0.25.5 + version: 0.25.5 + globals: + specifier: ^16.2.0 + version: 16.2.0 + jsdom: + specifier: ^29.0.2 + version: 29.0.2 + less: + specifier: ^4.3.0 + version: 4.3.0 + postcss: + specifier: ^8.5.10 + version: 8.5.10 + tailwindcss: + specifier: ^4.2.4 + version: 4.2.4 + tsc-alias: + specifier: ^1.0.7 + version: 1.8.16 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + tsx: + specifier: ^4.19.2 + version: 4.21.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vite: + specifier: ^8.0.10 + version: 8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0) + vitest: + specifier: ^4.1.5 + version: 4.1.5(@types/node@24.0.4)(jsdom@29.0.2)(vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0)) + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.7': + resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.7': + resolution: {integrity: sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@fortawesome/fontawesome-free@6.7.2': + resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==} + engines: {node: '>=6'} + + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@mongodb-js/saslprep@1.3.0': + resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tailwindcss/node@4.2.4': + resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + + '@tailwindcss/oxide-android-arm64@4.2.4': + resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.4': + resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.4': + resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.4': + resolution: {integrity: sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/browser-sync@2.29.0': + resolution: {integrity: sha512-d2V8FDX/LbDCSm343N2VChzDxvll0h76I8oSigYpdLgPDmcdcR6fywTggKBkUiDM3qAbHOq7NZvepj/HJM5e2g==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/compression@1.8.1': + resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie-parser@1.4.9': + resolution: {integrity: sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==} + peerDependencies: + '@types/express': '*' + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + + '@types/express-session@1.18.2': + resolution: {integrity: sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==} + + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + + '@types/geoip-lite@1.4.4': + resolution: {integrity: sha512-2uVfn+C6bX/H356H6mjxsWUA5u8LO8dJgSBIRO/NFlpMe4DESzacutD/rKYrTDKm1Ugv78b4Wz1KvpHrlv3jSw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/less@3.0.8': + resolution: {integrity: sha512-Gjm4+H9noDJgu5EdT3rUw5MhPBag46fiOy27BefvWkNL8mlZnKnCaVVVTLKj6RYXed9b62CPKnPav9govyQDzA==} + + '@types/luxon@3.6.2': + resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==} + + '@types/method-override@3.0.0': + resolution: {integrity: sha512-7XFHR6j7JljprBpzzRZatakUXm1kEGAM3PL/GSsGRHtDvOAKYCdmnXX/5YSl1eQrpJymGs9tRekSWEGaG+Ntjw==} + peerDependencies: + '@types/express': '*' + + '@types/micromatch@2.3.35': + resolution: {integrity: sha512-J749bHo/Zu56w0G0NI/IGHLQPiSsjx//0zJhfEVAN95K/xM5C8ZDmhkXtU3qns0sBOao7HuQzr8XV1/2o5LbXA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/morgan@1.9.10': + resolution: {integrity: sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/multer@1.4.13': + resolution: {integrity: sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==} + + '@types/node@24.0.4': + resolution: {integrity: sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==} + + '@types/nodemailer@6.4.17': + resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} + + '@types/numeral@2.0.5': + resolution: {integrity: sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==} + + '@types/parse-glob@3.0.32': + resolution: {integrity: sha512-n4xmml2WKR12XeQprN8L/sfiVPa8FHS3k+fxp4kSr/PA2GsGUgFND+bvISJxM0y5QdvzNEGjEVU3eIrcKks/pA==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-favicon@2.5.7': + resolution: {integrity: sha512-z9TNUQXdQ+W/OJMP1e3KOYUZ99qJS4+ZfFOIrPGImcayqKoyifbJSEFkVq1MCKBbqjMZpjPj3B5ilrQAR2+TOw==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + + '@types/uikit@3.14.5': + resolution: {integrity: sha512-3Q1CRzoAVLm+XNCP/2P5DbLuX98fkNLYxGQ1qUdV6cdzDTBbyVeH+est0LTZPtro9FUwXTNu8WfxfwLymFbZvQ==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansicolor@2.0.3: + resolution: {integrity: sha512-pzusTqk9VHrjgMCcTPDTTvfJfx6Q3+L5tQ6yKC8Diexmoit4YROTFIkxFvRTNL9y5s0Q8HrSrgerCD5bIC+Kiw==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-each-series@0.1.1: + resolution: {integrity: sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==} + engines: {node: '>=0.8.0'} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + babel-walk@3.0.0-canary-5: + resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} + engines: {node: '>= 10.0.0'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + baseline-browser-mapping@2.10.22: + resolution: {integrity: sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==} + engines: {node: '>=6.0.0'} + hasBin: true + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + + browser-sync-client@3.0.4: + resolution: {integrity: sha512-+ew5ubXzGRKVjquBL3u6najS40TG7GxCdyBll0qSRc/n+JRV9gb/yDdRL1IAgRHqjnJTdqeBKKIQabjvjRSYRQ==} + engines: {node: '>=8.0.0'} + + browser-sync-ui@3.0.4: + resolution: {integrity: sha512-5Po3YARCZ/8yQHFzvrSjn8+hBUF7ZWac39SHsy8Tls+7tE62iq6pYWxpVU6aOOMAGD21RwFQhQeqmJPf70kHEQ==} + + browser-sync@3.0.4: + resolution: {integrity: sha512-mcYOIy4BW6sWSEnTSBjQwWsnbx2btZX78ajTTjdNfyC/EqQVcIe0nQR6894RNAMtvlfAnLaH9L2ka97zpvgenA==} + engines: {node: '>= 8.0.0'} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-recipes@1.3.4: + resolution: {integrity: sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==} + + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bull@4.16.5: + resolution: {integrity: sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==} + engines: {node: '>=12'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001790: + resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-parser@2.2.0: + resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + + chart.js@4.5.0: + resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} + engines: {pnpm: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.0: + resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + connect-history-api-fallback@1.6.0: + resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==} + engines: {node: '>=0.8'} + + connect-redis@8.0.2: + resolution: {integrity: sha512-8A6rNOkwLQf25kkERnutNRppE3WUJ7B2bTl/DKf0jW9PkhzJQ3+VJYDHsN+NIuTtQrWsQr5n5UX7bm9l8rkTtQ==} + engines: {node: '>=18'} + peerDependencies: + express-session: '>=1' + + connect@3.6.6: + resolution: {integrity: sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==} + engines: {node: '>= 0.10.0'} + + constantinople@4.0.1: + resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + copy-anything@2.0.6: + resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + + cron@4.3.1: + resolution: {integrity: sha512-7x7DoEOxV11t3OPWWMjj1xrL1PGkTV5RV+/54IJTZD7gStiaMploY43EkeBSkDZTLRbUwk+OISbQ0TR133oXyA==} + engines: {node: '>=18.x'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + dev-ip@1.0.1: + resolution: {integrity: sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==} + engines: {node: '>= 0.8.0'} + hasBin: true + + diacritics@1.3.0: + resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctypes@1.1.0: + resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dotenv@16.6.0: + resolution: {integrity: sha512-Omf1L8paOy2VJhILjyhrhqwLIdstqm1BvcDPKg4NGAlkwEu9ODyrFbvk8UymUOMCT+HXo31jg1lArIrVAAhuGA==} + engines: {node: '>=12'} + + drange@1.1.1: + resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} + engines: {node: '>=4'} + + dtp-cleantext@1.0.0: + resolution: {integrity: sha512-V6+fTe/pv+w0+/z/P1ypjezUi9bBmuryuubAtXHknEG+PKLuWfPMsW/Xs7wyD3ZYUR7mLAXM4DXt1AiZ+PTKMw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + easy-extender@2.3.4: + resolution: {integrity: sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==} + engines: {node: '>= 4.0.0'} + + eazy-logger@4.1.0: + resolution: {integrity: sha512-+mn7lRm+Zf1UT/YaH8WXtpU6PIV2iOjzP6jgKoiaq/VNrjYKp+OHZGe2znaLgDeFkw8cL9ffuaUm+nNnzcYyGw==} + engines: {node: '>= 0.8.0'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.344: + resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.21.0: + resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} + engines: {node: '>=10.13.0'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express-session@1.18.1: + resolution: {integrity: sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==} + engines: {node: '>= 0.8.0'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-xml-parser@4.5.3: + resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + finalhandler@1.1.0: + resolution: {integrity: sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@3.0.1: + resolution: {integrity: sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + geoip-lite@1.4.10: + resolution: {integrity: sha512-4N69uhpS3KFd97m00wiFEefwa+L+HT5xZbzPhwu+sDawStg6UN/dPwWtUfkQuZkGIY1Cj7wDVp80IsqNtGMi2w==} + engines: {node: '>=10.3.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@16.2.0: + resolution: {integrity: sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-flag@5.0.1: + resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} + engines: {node: '>=12'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + immutable@3.8.2: + resolution: {integrity: sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==} + engines: {node: '>=0.10.0'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ioredis@5.6.0: + resolution: {integrity: sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==} + engines: {node: '>=12.22.0'} + + ip-address@5.9.4: + resolution: {integrity: sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==} + engines: {node: '>= 0.10'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-expression@4.0.0: + resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number-like@1.0.8: + resolution: {integrity: sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-what@3.14.1: + resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} + + is-wsl@1.1.0: + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} + + isnumber@1.0.0: + resolution: {integrity: sha512-JLiSz/zsZcGFXPrB4I/AGBvtStkt+8QmksyZBZnVXnnK9XdTEyz0tX8CRYljtwYDuIuZzih6DpHQdi+3Q6zHPw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-stringify@1.0.2: + resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jsdom@29.0.2: + resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsonfile@3.0.1: + resolution: {integrity: sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jstransformer@1.0.0: + resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + kareem@2.6.3: + resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} + engines: {node: '>=12.0.0'} + + lazy@1.0.11: + resolution: {integrity: sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==} + engines: {node: '>=0.2.0'} + + less@4.3.0: + resolution: {integrity: sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==} + engines: {node: '>=14'} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isfinite@3.3.2: + resolution: {integrity: sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + + luxon@3.6.1: + resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} + engines: {node: '>=12'} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + marked@16.0.0: + resolution: {integrity: sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA==} + engines: {node: '>= 20'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + method-override@3.0.0: + resolution: {integrity: sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==} + engines: {node: '>= 0.10'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minio@8.0.5: + resolution: {integrity: sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==} + engines: {node: ^16 || ^18 || >=20} + + mitt@1.2.0: + resolution: {integrity: sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mongodb-connection-string-url@3.0.2: + resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + + mongodb@6.17.0: + resolution: {integrity: sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + + mongoose@8.16.1: + resolution: {integrity: sha512-Q+0TC+KLdY4SYE+u9gk9pdW1tWu/pl0jusyEkMGTgBoAbvwQdfy4f9IM8dmvCwb/blSfp7IfLkob7v76x6ZGpQ==} + engines: {node: '>=16.20.1'} + + morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} + + mpath@0.9.0: + resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} + engines: {node: '>=4.0.0'} + + mquery@5.0.0: + resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} + engines: {node: '>=14.0.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.4: + resolution: {integrity: sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==} + + multer@2.0.1: + resolution: {integrity: sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==} + engines: {node: '>= 10.16.0'} + + mylas@2.1.14: + resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} + engines: {node: '>=16.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + needle@3.3.1: + resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} + engines: {node: '>= 4.4.x'} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + nodemailer@7.0.3: + resolution: {integrity: sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==} + engines: {node: '>=6.0.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + numeral@2.0.6: + resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ollama@0.6.3: + resolution: {integrity: sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + opn@5.3.0: + resolution: {integrity: sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==} + engines: {node: '>=4'} + + parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + portscanner@2.2.0: + resolution: {integrity: sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==} + engines: {node: '>=0.4', npm: '>=1.0.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + pug-attrs@3.0.0: + resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} + + pug-code-gen@3.0.3: + resolution: {integrity: sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==} + + pug-error@2.1.0: + resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} + + pug-filters@4.0.0: + resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} + + pug-lexer@5.0.1: + resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} + + pug-linker@4.0.0: + resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} + + pug-load@3.0.0: + resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} + + pug-parser@6.0.0: + resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} + + pug-runtime@3.0.1: + resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} + + pug-strip-comments@2.0.0: + resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} + + pug-walk@2.0.0: + resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} + + pug@3.0.3: + resolution: {integrity: sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + randexp@0.5.3: + resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==} + engines: {node: '>=4'} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-router-dom@7.14.2: + resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.14.2: + resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resp-modifier@6.0.2: + resolution: {integrity: sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==} + engines: {node: '>= 0.8.0'} + + ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rotating-file-stream@3.2.6: + resolution: {integrity: sha512-r8yShzMWUvWXkRzbOXDM1fEaMpc3qo2PzK7bBH/0p0Nl/uz8Mud/Y+0XTQxe3kbSnDF7qBH2tSe83WDKA7o3ww==} + engines: {node: '>=14.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rx@4.1.0: + resolution: {integrity: sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + send@0.19.1: + resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==} + engines: {node: '>= 0.8.0'} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-favicon@2.5.1: + resolution: {integrity: sha512-JndLBslCLA/ebr7rS3d+/EKkzTsTi1jI2T9l+vHfAaGJ7A7NhtDpSZ0lx81HCNWnnE0yHncG+SSnVf9IMxOwXQ==} + engines: {node: '>= 0.8.0'} + + serve-index@1.9.1: + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + server-destroy@1.0.1: + resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shoetest@1.2.2: + resolution: {integrity: sha512-iT8kIEFcGfUwo53VUFckm+glTkc0oLycRe+YqU/W4wQuIHGIWc5KMIpDnJVdavKCyEZKQTi8IDq27rDmB09QjA==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + sift@17.1.3: + resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + sprintf-js@1.1.2: + resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + stats-lite@2.2.0: + resolution: {integrity: sha512-/Kz55rgUIv2KP2MKphwYT/NCuSfAlbbMRv2ZWw7wyXayu230zdtzhxxuXXcvsc6EmmhS8bSJl3uS1wmMHFumbA==} + engines: {node: '>=2.0.0'} + + statuses@1.3.1: + resolution: {integrity: sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==} + engines: {node: '>= 0.6'} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + stream-throttle@0.1.3: + resolution: {integrity: sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==} + engines: {node: '>= 0.10.0'} + hasBin: true + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + striptags@3.2.0: + resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} + + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwindcss@4.2.4: + resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-stream@1.0.0: + resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ua-parser-js@1.0.40: + resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} + hasBin: true + + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + + uikit@3.23.11: + resolution: {integrity: sha512-srUFBf5DfUxVpodcygibMQt1vgQjR9wlhIQo4GeWVpugk5+mKLPASJITDoY8wcwXQIHm7koELiPJ+FgNbzLv0A==} + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + with@7.0.2: + resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} + engines: {node: '>= 10.0.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + 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 + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@alloc/quick-lru@5.2.0': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.27.7': + dependencies: + '@babel/types': 7.27.7 + + '@babel/runtime@7.29.2': {} + + '@babel/types@7.27.7': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@exodus/bytes@1.15.0': {} + + '@fortawesome/fontawesome-free@6.7.2': {} + + '@ioredis/commands@1.2.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kurkle/color@0.3.4': {} + + '@mongodb-js/saslprep@1.3.0': + dependencies: + sparse-bitfield: 3.0.3 + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/types@0.127.0': {} + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.17': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@socket.io/component-emitter@3.1.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@tailwindcss/node@4.2.4': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.0 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.4 + + '@tailwindcss/oxide-android-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide@4.2.4': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-x64': 4.2.4 + '@tailwindcss/oxide-freebsd-x64': 4.2.4 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-x64-musl': 4.2.4 + '@tailwindcss/oxide-wasm32-wasi': 4.2.4 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + + '@tailwindcss/postcss@4.2.4': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.4 + '@tailwindcss/oxide': 4.2.4 + postcss: 8.5.10 + tailwindcss: 4.2.4 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.0.4 + + '@types/browser-sync@2.29.0': + dependencies: + '@types/micromatch': 2.3.35 + '@types/node': 24.0.4 + '@types/serve-static': 1.15.8 + chokidar: 3.6.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/compression@1.8.1': + dependencies: + '@types/express': 5.0.3 + '@types/node': 24.0.4 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.0.4 + + '@types/cookie-parser@1.4.9(@types/express@5.0.3)': + dependencies: + '@types/express': 5.0.3 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 24.0.4 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.0.6': + dependencies: + '@types/node': 24.0.4 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express-session@1.18.2': + dependencies: + '@types/express': 5.0.3 + + '@types/express@5.0.3': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.0.6 + '@types/serve-static': 1.15.8 + + '@types/geoip-lite@1.4.4': {} + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.0.4 + + '@types/less@3.0.8': {} + + '@types/luxon@3.6.2': {} + + '@types/method-override@3.0.0(@types/express@5.0.3)': + dependencies: + '@types/express': 5.0.3 + + '@types/micromatch@2.3.35': + dependencies: + '@types/parse-glob': 3.0.32 + + '@types/mime@1.3.5': {} + + '@types/morgan@1.9.10': + dependencies: + '@types/node': 24.0.4 + + '@types/ms@2.1.0': {} + + '@types/multer@1.4.13': + dependencies: + '@types/express': 5.0.3 + + '@types/node@24.0.4': + dependencies: + undici-types: 7.8.0 + + '@types/nodemailer@6.4.17': + dependencies: + '@types/node': 24.0.4 + + '@types/numeral@2.0.5': {} + + '@types/parse-glob@3.0.32': {} + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 24.0.4 + + '@types/serve-favicon@2.5.7': + dependencies: + '@types/express': 5.0.3 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.0.4 + '@types/send': 0.17.5 + + '@types/uikit@3.14.5': {} + + '@types/uuid@10.0.0': {} + + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + + '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0) + + '@vitest/expect@4.1.5': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0) + + '@vitest/pretty-format@4.1.5': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.5': + dependencies: + '@vitest/utils': 4.1.5 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.5': {} + + '@vitest/utils@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@zxing/text-encoding@0.9.0': + optional: true + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + acorn@7.4.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansicolor@2.0.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + append-field@1.0.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-union@2.1.0: {} + + asap@2.0.6: {} + + assert-never@1.4.0: {} + + assertion-error@2.0.1: {} + + async-each-series@0.1.1: {} + + async@2.6.4: + dependencies: + lodash: 4.17.21 + + async@3.2.6: {} + + autoprefixer@10.5.0(postcss@8.5.10): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001790 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.10 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + babel-walk@3.0.0-canary-5: + dependencies: + '@babel/types': 7.27.7 + + balanced-match@1.0.2: {} + + base64id@2.0.0: {} + + baseline-browser-mapping@2.10.22: {} + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + batch@0.6.1: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + binary-extensions@2.3.0: {} + + block-stream2@2.1.0: + dependencies: + readable-stream: 3.6.2 + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-or-node@2.1.1: {} + + browser-sync-client@3.0.4: + dependencies: + etag: 1.8.1 + fresh: 0.5.2 + mitt: 1.2.0 + + browser-sync-ui@3.0.4: + dependencies: + async-each-series: 0.1.1 + chalk: 4.1.2 + connect-history-api-fallback: 1.6.0 + immutable: 3.8.2 + server-destroy: 1.0.1 + socket.io-client: 4.8.3 + stream-throttle: 0.1.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + browser-sync@3.0.4: + dependencies: + browser-sync-client: 3.0.4 + browser-sync-ui: 3.0.4 + bs-recipes: 1.3.4 + chalk: 4.1.2 + chokidar: 3.6.0 + connect: 3.6.6 + connect-history-api-fallback: 1.6.0 + dev-ip: 1.0.1 + easy-extender: 2.3.4 + eazy-logger: 4.1.0 + etag: 1.8.1 + fresh: 0.5.2 + fs-extra: 3.0.1 + http-proxy: 1.18.1 + immutable: 3.8.2 + micromatch: 4.0.8 + opn: 5.3.0 + portscanner: 2.2.0 + raw-body: 2.5.2 + resp-modifier: 6.0.2 + rx: 4.1.0 + send: 0.19.1 + serve-index: 1.9.1 + serve-static: 1.16.2 + server-destroy: 1.0.1 + socket.io: 4.8.3 + ua-parser-js: 1.0.40 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.22 + caniuse-lite: 1.0.30001790 + electron-to-chromium: 1.5.344 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bs-recipes@1.3.4: {} + + bson@6.10.4: {} + + buffer-crc32@0.2.13: {} + + buffer-crc32@1.0.0: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + bull@4.16.5: + dependencies: + cron-parser: 4.9.0 + get-port: 5.1.1 + ioredis: 5.6.0 + lodash: 4.17.21 + msgpackr: 1.11.4 + semver: 7.7.2 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + 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 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001790: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-parser@2.2.0: + dependencies: + is-regex: 1.2.1 + + chart.js@4.5.0: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cluster-key-slot@1.1.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@2.20.3: {} + + commander@9.5.0: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.52.0 + + compression@1.8.0: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.0.2 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + connect-history-api-fallback@1.6.0: {} + + connect-redis@8.0.2(express-session@1.18.1): + dependencies: + express-session: 1.18.1 + + connect@3.6.6: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.0 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + constantinople@4.0.1: + dependencies: + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + copy-anything@2.0.6: + dependencies: + is-what: 3.14.1 + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cron-parser@4.9.0: + dependencies: + luxon: 3.6.1 + + cron@4.3.1: + dependencies: + '@types/luxon': 3.6.2 + luxon: 3.6.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + dayjs@1.11.13: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.1.0: + dependencies: + ms: 2.0.0 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + decode-uri-component@0.2.2: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + denque@2.1.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@2.0.4: {} + + dev-ip@1.0.1: {} + + diacritics@1.3.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctypes@1.1.0: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dotenv@16.6.0: {} + + drange@1.1.1: {} + + dtp-cleantext@1.0.0: + dependencies: + diacritics: 1.3.0 + shoetest: 1.2.2 + stats-lite: 2.2.0 + striptags: 3.2.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + easy-extender@2.3.4: + dependencies: + lodash: 4.17.21 + + eazy-logger@4.1.0: + dependencies: + chalk: 4.1.2 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.344: {} + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + engine.io-client@6.6.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 24.0.4 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@5.21.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@8.0.0: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.0.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.1: {} + + expect-type@1.3.0: {} + + express-rate-limit@7.5.1(express@5.1.0): + dependencies: + express: 5.1.0 + + express-session@1.18.1: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.0.2 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-xml-parser@4.5.3: + dependencies: + strnum: 1.1.2 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@1.1.0: {} + + finalhandler@1.1.0: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.3.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.15.9: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + forwarded@0.2.0: {} + + fraction.js@5.3.4: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fs-extra@3.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 3.0.1 + universalify: 0.1.2 + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + geoip-lite@1.4.10: + dependencies: + async: 2.6.4 + chalk: 4.1.2 + iconv-lite: 0.6.3 + ip-address: 5.9.4 + lazy: 1.0.11 + rimraf: 2.7.1 + yauzl: 2.10.0 + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + 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 + + get-port@5.1.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@16.2.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-flag@5.0.1: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + + http-errors@1.6.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.9 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + image-size@0.5.5: + optional: true + + immutable@3.8.2: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ioredis@5.6.0: + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.4.1 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ip-address@5.9.4: + dependencies: + jsbn: 1.1.0 + lodash: 4.17.21 + sprintf-js: 1.1.2 + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.2.0: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-expression@4.0.0: + dependencies: + acorn: 7.4.1 + object-assign: 4.1.1 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number-like@1.0.8: + dependencies: + lodash.isfinite: 3.3.2 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-promise@2.2.2: {} + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-what@3.14.1: {} + + is-wsl@1.1.0: {} + + isnumber@1.0.0: {} + + jiti@2.6.1: {} + + js-stringify@1.0.2: {} + + js-tokens@4.0.0: {} + + jsbn@1.1.0: {} + + jsdom@29.0.2: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsonfile@3.0.1: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.2 + + jstransformer@1.0.0: + dependencies: + is-promise: 2.2.2 + promise: 7.3.1 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + + kareem@2.6.3: {} + + lazy@1.0.11: {} + + less@4.3.0: + dependencies: + copy-anything: 2.0.6 + parse-node-version: 1.0.1 + tslib: 2.8.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.3.1 + source-map: 0.6.1 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + limiter@1.1.5: {} + + lodash.defaults@4.2.0: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isfinite@3.3.2: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lodash@4.17.21: {} + + lru-cache@11.3.5: {} + + luxon@3.6.1: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + optional: true + + marked@16.0.0: {} + + math-intrinsics@1.1.0: {} + + mdn-data@2.27.1: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memory-pager@1.5.0: {} + + merge-descriptors@2.0.0: {} + + merge2@1.4.1: {} + + method-override@3.0.0: + dependencies: + debug: 3.1.0 + methods: 1.1.2 + parseurl: 1.3.3 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + minio@8.0.5: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 1.0.0 + eventemitter3: 5.0.1 + fast-xml-parser: 4.5.3 + ipaddr.js: 2.2.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + stream-json: 1.9.1 + through2: 4.0.2 + web-encoding: 1.1.5 + xml2js: 0.6.2 + + mitt@1.2.0: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mongodb-connection-string-url@3.0.2: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 14.2.0 + + mongodb@6.17.0: + dependencies: + '@mongodb-js/saslprep': 1.3.0 + bson: 6.10.4 + mongodb-connection-string-url: 3.0.2 + + mongoose@8.16.1: + dependencies: + bson: 6.10.4 + kareem: 2.6.3 + mongodb: 6.17.0 + mpath: 0.9.0 + mquery: 5.0.0 + ms: 2.1.3 + sift: 17.1.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + + morgan@1.10.0: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + + mpath@0.9.0: {} + + mquery@5.0.0: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.4: + optionalDependencies: + msgpackr-extract: 3.0.3 + + multer@2.0.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + + mylas@2.1.14: {} + + nanoid@3.3.11: {} + + needle@3.3.1: + dependencies: + iconv-lite: 0.6.3 + sax: 1.4.1 + optional: true + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.0.4 + optional: true + + node-releases@2.0.38: {} + + nodemailer@7.0.3: {} + + normalize-path@3.0.0: {} + + numeral@2.0.6: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obug@2.1.1: {} + + ollama@0.6.3: + dependencies: + whatwg-fetch: 3.6.20 + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + opn@5.3.0: + dependencies: + is-wsl: 1.1.0 + + parse-node-version@1.0.1: {} + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + parseurl@1.3.3: {} + + path-is-absolute@1.0.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@8.2.0: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.4: {} + + pify@4.0.1: + optional: true + + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + + portscanner@2.2.0: + dependencies: + async: 2.6.4 + is-number-like: 1.0.8 + + possible-typed-array-names@1.1.0: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + promise@7.3.1: + dependencies: + asap: 2.0.6 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + prr@1.0.1: + optional: true + + pug-attrs@3.0.0: + dependencies: + constantinople: 4.0.1 + js-stringify: 1.0.2 + pug-runtime: 3.0.1 + + pug-code-gen@3.0.3: + dependencies: + constantinople: 4.0.1 + doctypes: 1.1.0 + js-stringify: 1.0.2 + pug-attrs: 3.0.0 + pug-error: 2.1.0 + pug-runtime: 3.0.1 + void-elements: 3.1.0 + with: 7.0.2 + + pug-error@2.1.0: {} + + pug-filters@4.0.0: + dependencies: + constantinople: 4.0.1 + jstransformer: 1.0.0 + pug-error: 2.1.0 + pug-walk: 2.0.0 + resolve: 1.22.10 + + pug-lexer@5.0.1: + dependencies: + character-parser: 2.2.0 + is-expression: 4.0.0 + pug-error: 2.1.0 + + pug-linker@4.0.0: + dependencies: + pug-error: 2.1.0 + pug-walk: 2.0.0 + + pug-load@3.0.0: + dependencies: + object-assign: 4.1.1 + pug-walk: 2.0.0 + + pug-parser@6.0.0: + dependencies: + pug-error: 2.1.0 + token-stream: 1.0.0 + + pug-runtime@3.0.1: {} + + pug-strip-comments@2.0.0: + dependencies: + pug-error: 2.1.0 + + pug-walk@2.0.0: {} + + pug@3.0.3: + dependencies: + pug-code-gen: 3.0.3 + pug-filters: 4.0.0 + pug-lexer: 5.0.1 + pug-linker: 4.0.0 + pug-load: 3.0.0 + pug-parser: 6.0.0 + pug-runtime: 3.0.1 + pug-strip-comments: 2.0.0 + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + queue-lit@1.5.2: {} + + queue-microtask@1.2.3: {} + + randexp@0.5.3: + dependencies: + drange: 1.1.1 + ret: 0.2.2 + + random-bytes@1.0.0: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + + react@19.2.5: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resp-modifier@6.0.2: + dependencies: + debug: 2.6.9 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + ret@0.2.2: {} + + reusify@1.1.0: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + + rotating-file-stream@3.2.6: {} + + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rx@4.1.0: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + sax@1.4.1: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@5.7.2: + optional: true + + semver@7.7.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + send@0.19.1: + 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.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-favicon@2.5.1: + dependencies: + etag: 1.8.1 + fresh: 0.5.2 + ms: 2.1.3 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + + serve-index@1.9.1: + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + server-destroy@1.0.1: {} + + set-cookie-parser@2.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.1.0: {} + + setprototypeof@1.2.0: {} + + shoetest@1.2.2: + dependencies: + randexp: 0.5.3 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + sift@17.1.3: {} + + siginfo@2.0.0: {} + + slash@3.0.0: {} + + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.7 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.8.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.1 + engine.io-client: 6.6.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.3: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.4.1 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + source-map-js@1.2.1: {} + + source-map@0.6.1: + optional: true + + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + + split-on-first@1.1.0: {} + + sprintf-js@1.1.2: {} + + stackback@0.0.2: {} + + standard-as-callback@2.1.0: {} + + stats-lite@2.2.0: + dependencies: + isnumber: 1.0.0 + + statuses@1.3.1: {} + + statuses@1.5.0: {} + + statuses@2.0.1: {} + + std-env@4.1.0: {} + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + stream-throttle@0.1.3: + dependencies: + commander: 2.20.3 + limiter: 1.1.5 + + streamsearch@1.1.0: {} + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + striptags@3.2.0: {} + + strnum@1.1.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tailwindcss@4.2.4: {} + + tapable@2.3.3: {} + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + tinybench@2.9.0: {} + + tinyexec@1.1.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tldts-core@7.0.28: {} + + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-stream@1.0.0: {} + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + tsc-alias@1.8.16: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.14.0 + globby: 11.1.0 + mylas: 2.1.14 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typedarray@0.0.6: {} + + typescript@5.8.3: {} + + ua-parser-js@1.0.40: {} + + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + + uikit@3.23.11: {} + + undici-types@7.8.0: {} + + undici@7.25.0: {} + + universalify@0.1.2: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + uuid@8.3.2: {} + + vary@1.1.2: {} + + vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.0.4 + esbuild: 0.25.5 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.3.0 + tsx: 4.21.0 + + vitest@4.1.5(@types/node@24.0.4)(jsdom@29.0.2)(vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.0.4 + jsdom: 29.0.2 + transitivePeerDependencies: + - msw + + void-elements@3.1.0: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + + webidl-conversions@7.0.0: {} + + webidl-conversions@8.0.1: {} + + whatwg-fetch@3.6.20: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + with@7.0.2: + dependencies: + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 + assert-never: 1.4.0 + babel-walk: 3.0.0-canary-5 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.17.1: {} + + xml-name-validator@5.0.0: {} + + xml2js@0.6.2: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlchars@2.2.0: {} + + xmlhttprequest-ssl@2.1.2: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 diff --git a/gadget-code/pnpm-workspace.yaml b/gadget-code/pnpm-workspace.yaml new file mode 100644 index 0000000..f461f99 --- /dev/null +++ b/gadget-code/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +onlyBuiltDependencies: + - esbuild + - msgpackr-extract + - vue-demi diff --git a/gadget-code/release b/gadget-code/release new file mode 100755 index 0000000..57ffaf7 --- /dev/null +++ b/gadget-code/release @@ -0,0 +1,54 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status +set -e + +PRODUCTION_BRANCH="master" +DEVELOP_BRANCH="develop" + +REMOTE_NAME=${2:-origin} +RELEASE_TYPE=$1 + +# 1. Check for required argument +if [ -z "$RELEASE_TYPE" ]; then + echo "❌ Error: Must specify release type (major, minor, patch)." + echo "Usage: ./release [remote_name]" + exit 1 +fi + +# 2. Verify the remote exists +if ! git remote | grep -q "^$REMOTE_NAME$"; then + echo "❌ Error: Remote '$REMOTE_NAME' not found." + echo "Available remotes: $(git remote | tr '\n' ' ')" + exit 1 +fi + +# 3. Ensure working directory is clean (no uncommitted changes) +if [[ -n $(git status --porcelain) ]]; then + echo "❌ Error: Your working directory is dirty. Commit or stash changes first." + exit 1 +fi + +echo "🚀 Starting release on $REMOTE_NAME..." + +# 4. Sync Develop +git checkout "$DEVELOP_BRANCH" +git pull "$REMOTE_NAME" "$DEVELOP_BRANCH" +npm version "$RELEASE_TYPE" +git push "$REMOTE_NAME" "$DEVELOP_BRANCH" --follow-tags + +# 5. Sync Production +git checkout "$PRODUCTION_BRANCH" +git pull "$REMOTE_NAME" "$PRODUCTION_BRANCH" + +# Merge develop into production locally +# Using --no-ff preserves the release history in the git graph +git merge "$DEVELOP_BRANCH" --no-edit + +# 6. Push Production +git push "$REMOTE_NAME" "$PRODUCTION_BRANCH" + +# 7. Return to Develop +git checkout "$DEVELOP_BRANCH" + +echo "✅ Release complete!" diff --git a/gadget-code/src/config/browsersync.ts b/gadget-code/src/config/browsersync.ts new file mode 100644 index 0000000..dceb980 --- /dev/null +++ b/gadget-code/src/config/browsersync.ts @@ -0,0 +1,34 @@ +// browsersync.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "./env.js"; + +import { Options as BrowserSyncOptions } from "browser-sync"; + +const options: BrowserSyncOptions = { + proxy: { + target: `https://${env.site.host}/`, + ws: true, + }, + host: env.site.domain, + open: false, + https: { + key: env.https.keyFile, + cert: env.https.crtFile, + }, + port: 3000, + cors: true, + ui: { + port: 3620, + }, + notify: false, + ghostMode: { + clicks: false, + forms: false, + scroll: true, + }, + logLevel: "info", +}; + +export default options; diff --git a/gadget-code/src/config/env.ts b/gadget-code/src/config/env.ts new file mode 100644 index 0000000..46e9c77 --- /dev/null +++ b/gadget-code/src/config/env.ts @@ -0,0 +1,167 @@ +// config/env.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import "dotenv/config"; +import type PackageJson from "../../package.json"; + +import assert from "node:assert"; +assert(process.env.DTP_USER_PASSWORD_SALT, "must define password salt in .env"); +assert(process.env.DTP_JWT_SECRET, "must define JSON Web Token secret"); + +import path, { dirname } from "node:path"; +import fs from "node:fs"; +import { fileURLToPath } from "node:url"; +const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line + +export const ROOT_DIR = path.resolve(__dirname, "..", ".."); +export const SRC_DIR = path.resolve(__dirname, ".."); + +async function readJsonFile(path: string): Promise { + const file = await fs.promises.readFile(path); + return JSON.parse(file.toString("utf-8")) as T; +} + +export interface ISiteDefinition { + company: string; + companyShort: string; + name: string; + shortName: string; + tagline: string; + sloagn: string; + description: string; + domain: string; + domainKey: string; + host: string; +} + +export default { + NODE_ENV: process.env.NODE_ENV, + timezone: process.env.DTP_TIMEZONE || "America/New_York", + root: ROOT_DIR, + src: SRC_DIR, + pkg: await readJsonFile( + path.join(ROOT_DIR, "package.json"), + ), + site: { + company: process.env.DTP_SITE_COMPANY || "Robert Colbert", + companyShort: process.env.DTP_SITE_COMPANY_SHORT || "Colbert", + name: process.env.DTP_SITE_NAME || "Gadget Code", + shortName: process.env.DTP_SITE_NAME || "Gadget Code", + slogan: process.env.DTP_SITE_SLOGAN || "Self-hosted Agentic Engineering Platform", + description: + process.env.DTP_SITE_DESCRIPTION || + "Gadget Code - A self-hosted Agentic Engineering Platform (AEP).", + domain: process.env.DTP_SITE_DOMAIN || "code-dev.g4dge7.com", + domainKey: + process.env.DTP_SITE_DOMAIN_KEY || "code-dev.g4dge7.com", + host: process.env.DTP_SITE_HOST || "code-dev.g4dge7.com", + }, + ai: { + ollama: { + apiUrl: process.env.DTP_OLLAMA_API_URL || "http://localhost:11434", + apiKey: process.env.DTP_OLLAMA_API_KEY || "", + }, + }, + auth: { + jwtSecret: process.env.DTP_JWT_SECRET, + }, + session: { + secret: process.env.DTP_SESSION_SECRET, + trustProxy: + process.env.NODE_ENV === "production" || + process.env.DTP_SESSION_TRUST_PROXY === "enabled", + cookie: { + secure: process.env.DTP_SESSION_COOKIE_SECURE === "enabled", + sameSite: process.env.DTP_SESSION_COOKIE_SAMESITE || false, + }, + }, + mongodb: { + host: process.env.DTP_MONGODB_HOST || "localhost", + database: process.env.DTP_MONGODB_DATABASE || "", + }, + redis: { + host: process.env.DTP_REDIS_HOST || "localhost", + port: parseInt(process.env.DTP_REDIS_PORT || "6379", 10), + password: process.env.DTP_REDIS_PASSWORD, + keyPrefix: process.env.DTP_REDIS_KEY_PREFIX || "dtp", + lazyConnect: process.env.DTP_REDIS_LAZYCONNECT === "enabled", + }, + minio: { + endpoint: process.env.DTP_MINIO_ENDPOINT || "localhost", + port: parseInt(process.env.DTP_MINIO_PORT || "9080", 10), + useSsl: process.env.DTP_MINIO_USE_SSL === "enabled", + accessKey: process.env.DTP_MINIO_ACCESS_KEY, + secretKey: process.env.DTP_MINIO_SECRET_KEY, + buckets: { + uploads: process.env.DTP_MINIO_UPLOAD_BUCKET || "dtp-uploads", + images: process.env.DTP_MINIO_IMAGE_BUCKET || "dtp-images", + videos: process.env.DTP_MINIO_VIDEO_BUCKET || "dtp-videos", + audios: process.env.DTP_MINIO_AUDIO_BUCKET || "dtp-audios", + }, + }, + user: { + signupEnabled: process.env.DTP_USER_SIGNUP === "enabled", + passwordSalt: process.env.DTP_USER_PASSWORD_SALT, + }, + https: { + enabled: process.env.DTP_HTTPS === "enabled", + address: process.env.DTP_HTTPS_HOST || "127.0.0.1", + port: parseInt(process.env.DTP_HTTPS_PORT || "3443", 10), + backlog: parseInt(process.env.DTP_HTTPS_BACKLOG || "16", 10), + keyFile: process.env.DTP_HTTPS_KEY_FILE, + crtFile: process.env.DTP_HTTPS_CRT_FILE, + uploadPath: process.env.DTP_HTTPS_UPLOAD_PATH || "/tmp", + }, + frontend: { + port: 5173, + }, + email: { + enabled: process.env.DTP_EMAIL_SERVICE === "enabled", + smtp: { + host: process.env.DTP_EMAIL_SMTP_HOST || "localhost", + port: parseInt(process.env.DTP_EMAIL_SMTP_PORT || "465", 10), + secure: process.env.DTP_EMAIL_SMTP_SECURE === "enabled", + from: + process.env.DTP_EMAIL_SMTP_FROM || + "Digital Telepresence Support ", + user: process.env.DTP_EMAIL_SMTP_USER, + password: process.env.DTP_EMAIL_SMTP_PASS, + pool: { + enabled: process.env.DTP_EMAIL_SMTP_POOL_ENABLED === "enabled", + maxConnections: parseInt( + process.env.DTP_EMAIL_SMTP_POOL_MAX_CONN || "5", + 10, + ), + maxMessages: parseInt( + process.env.DTP_EMAIL_SMTP_POOL_MAX_MSGS || "100", + 10, + ), + }, + }, + contact: { + to: + process.env.DTP_EMAIL_CONTACT_TO || + "DTP Support ", + }, + }, + log: { + https: { + enabled: process.env.DTP_LOG_HTTPS === "enabled" || false, + name: process.env.DTP_LOG_HTTPS_NAME || "gadget-code-https.log", + path: process.env.DTP_LOG_HTTPS_PATH || "/var/log/dtp", + format: process.env.DTP_LOG_HTTPS_FORMAT || "combined", + }, + console: { + enabled: process.env.DTP_LOG_CONSOLE === "enabled", + }, + file: { + enabled: process.env.DTP_LOG_FILE === "enabled", + }, + levels: { + debug: process.env.DTP_LOG_DEBUG === "enabled", + info: process.env.DTP_LOG_INFO === "enabled", + warn: process.env.DTP_LOG_WARN === "enabled", + }, + }, +}; diff --git a/gadget-code/src/controllers/api.ts b/gadget-code/src/controllers/api.ts new file mode 100644 index 0000000..9593249 --- /dev/null +++ b/gadget-code/src/controllers/api.ts @@ -0,0 +1,33 @@ +// src/controllers/api.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../config/env.js"; +import path from "node:path"; + +import { DtpController } from "../lib/controller.js"; + +export class ApiController extends DtpController { + get name(): string { + return "ApiController"; + } + get slug(): string { + return "api"; + } + get route(): string { + return "/api"; + } + + constructor() { + super(); + } + + async start(): Promise { + const envDir = env.NODE_ENV === "production" ? "dist" : "src"; + await this.loadChild( + path.join(env.root, envDir, "controllers", "api", "v1.js") + ); + } +} + +export default ApiController; diff --git a/gadget-code/src/controllers/api/v1.ts b/gadget-code/src/controllers/api/v1.ts new file mode 100644 index 0000000..1b292fe --- /dev/null +++ b/gadget-code/src/controllers/api/v1.ts @@ -0,0 +1,39 @@ +// src/controllers/api/v1.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../../config/env.js"; +import path from "node:path"; + +import { DtpController } from "../../lib/controller.js"; + +export class ApiControllerV1 extends DtpController { + get name(): string { + return "ApiControllerV1"; + } + get slug(): string { + return "v1"; + } + get route(): string { + return "/v1"; + } + + constructor() { + super(); + } + + async start(): Promise { + let basePath; + if (env.NODE_ENV === "production") { + basePath = path.join(env.root, "dist", "controllers", "api", "v1"); + } else { + basePath = path.join(env.root, "src", "controllers", "api", "v1"); + } + await this.loadChild(path.join(basePath, "auth.js")); + await this.loadChild(path.join(basePath, "contact.js")); + await this.loadChild(path.join(basePath, "drone.js")); + await this.loadChild(path.join(basePath, "user.js")); + } +} + +export default ApiControllerV1; diff --git a/gadget-code/src/controllers/api/v1/auth.ts b/gadget-code/src/controllers/api/v1/auth.ts new file mode 100644 index 0000000..57b7d56 --- /dev/null +++ b/gadget-code/src/controllers/api/v1/auth.ts @@ -0,0 +1,142 @@ +// src/controllers/api/v1/auth.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +// import env from "../config/env.js"; +import { Request, Response } from "express"; + +import { DtpController } from "../../../lib/controller.js"; + +import UserService from "../../../services/user.js"; +import SessionService, { SessionType } from "../../../services/session.js"; + +export class AuthApiControllerV1 extends DtpController { + get name(): string { + return "AuthApiControllerV1"; + } + get slug(): string { + return "authV1"; + } + get route(): string { + return "/auth"; + } + + constructor() { + super(); + } + + async start(): Promise { + this.router.post("/sign-in", this.postSignIn.bind(this)); + this.router.post("/renew-token", this.postRenewToken.bind(this)); + this.router.get("/sign-out", this.getSignOut.bind(this)); + } + + async postSignIn(req: Request, res: Response): Promise { + try { + this.log.info("authenticating user account", { + email: req.body.email, + }); + const user = await UserService.authenticate( + req.body.email, + req.body.password + ); + req.session.user = { + _id: user._id, + email: user.email, + displayName: user.displayName, + flags: user.flags, + }; + + this.log.info("creating JSON Web Token for user session", { + user: { _id: user._id }, + }); + const token = await SessionService.createJsonWebToken(user); + req.session.token = token; + req.session.type = SessionType.JWT; + + req.session.save((err: Error): void => { + if (err) { + res.status(err.statusCode || 500).json({ + success: false, + message: err.message, + }); + return; + } + res.status(200).json({ + success: true, + user: { + _id: user._id.toString(), + email: user.email, + displayName: user.displayName, + flags: user.flags, + }, + token, + }); + }); + } catch (error) { + this.log.error("failed to process user sign-in", { error }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + } + } + + async postRenewToken(req: Request, res: Response): Promise { + try { + const user = await SessionService.verifyJsonWebToken(req.body.token); + const token = await SessionService.createJsonWebToken(user); + req.session.token = token; + this.log.info("user session token renewed", { + user: { + _id: user._id, + displayName: user.displayName, + flags: user.flags, + }, + }); + res.status(200).json({ success: true, token }); + } catch (error) { + this.log.error("failed to process token renewal", { error }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + } + } + + async getSignOut(req: Request, res: Response): Promise { + const user = req.user; + if (req.session.token) { + try { + await SessionService.revokeJsonWebToken(req.session.token); + } catch (error) { + this.log.error("failed to revoke user session", { error }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + return; + } + } + req.session.destroy((err: Error): void => { + if (err) { + this.log.error("failed to destroy user session", { error: err }); + res.status(200).json({ + success: false, + message: err.message, + }); + return; + } + if (user) { + this.log.info("user session destroyed (sign out)", { + user: { _id: user._id }, + }); + } else { + this.log.info("userless session destroyed (sign out)"); + } + res.status(200).json({ success: true }); + }); + } +} + +export default AuthApiControllerV1; diff --git a/gadget-code/src/controllers/api/v1/contact.ts b/gadget-code/src/controllers/api/v1/contact.ts new file mode 100644 index 0000000..f4f9322 --- /dev/null +++ b/gadget-code/src/controllers/api/v1/contact.ts @@ -0,0 +1,82 @@ +// src/controllers/api/v1/contact.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +// import env from "../../../config/env.js"; + +import { Request, Response } from "express"; +// import ContactService from "../../../services/contact.js"; + +import { DtpController } from "../../../lib/controller.js"; + +export class ContactApiControllerV1 extends DtpController { + get name(): string { + return "ContactApiControllerV1"; + } + get slug(): string { + return "contactV1"; + } + get route(): string { + return "/contact"; + } + + constructor() { + super(); + } + + async start(): Promise { + const upload = this.createMulter(this.slug, { + dest: "/tmp/", + limits: { + fileSize: 1024 * 1024 * 10, + }, + }); + + const limiter = this.createLimiter( + 60 * 60, // 1-hour time window + 3, + "Too many messages sent. Please try again later.", + (req: Request, _res: Response) => { + if (!req.body || !req.body.email) { + return req.ip; + } + return req.ip + req.body.email; + } + ); + + this.router.post( + "/send-email", + limiter, + upload.single("attachment"), + this.postSendEmail.bind(this) + ); + } + + async postSendEmail(_req: Request, res: Response): Promise { + try { + // const jobs = [ + // ContactService.sendContactUsMessage( + // req, + // env.email.contact.to, + // `You have received a contact message at ${env.site.name}.` + // ), + // ContactService.sendContactUsMessage( + // req, + // req.body.email, + // `${env.site.name} has received your message. A representative will be in touch as soon as possible.` + // ), + // ]; + // await Promise.all(jobs); + + res.status(200).json({ success: true }); + } catch (error) { + this.log.error("failed to send email message", { error }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + } + } +} + +export default ContactApiControllerV1; diff --git a/gadget-code/src/controllers/api/v1/drone.ts b/gadget-code/src/controllers/api/v1/drone.ts new file mode 100644 index 0000000..f55a398 --- /dev/null +++ b/gadget-code/src/controllers/api/v1/drone.ts @@ -0,0 +1,99 @@ +// src/controllers/api/v1/drone.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Request, Response } from "express"; + +import DroneService from "../../../services/drone.ts"; +import UserService from "../../../services/user.ts"; + +import { DtpController } from "../../../lib/controller.js"; +import { populateDroneRegistrationById } from "@/controllers/lib/populators.js"; + +export class DroneApiControllerV1 extends DtpController { + get name(): string { + return "DroneApiControllerV1"; + } + get slug(): string { + return "ctrl:drone:v1"; + } + get route(): string { + return "/drone"; + } + + constructor() { + super(); + } + + async start(): Promise { + const multer = this.createMulter(this.slug, {}); + this.router.param("registrationId", populateDroneRegistrationById(this)); + + this.router.post( + "/registration", + multer.none(), + this.postRegistration.bind(this), + ); + + this.router.put( + "/registration/:registrationId/status", + this.putRegistrationStatus.bind(this), + ); + + this.router.delete( + "/registration/:registrationId", + this.deleteRegistration.bind(this), + ); + } + + async postRegistration(req: Request, res: Response): Promise { + try { + const user = await UserService.authenticate( + req.body.email, + req.body.password, + ); + const registration = await DroneService.register(user, req.body); + res.status(200).json({ + success: true, + data: registration, + }); + } catch (error) { + this.log.error("failed to process drone registration", { error }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + } + } + + async putRegistrationStatus(req: Request, res: Response): Promise { + try { + await DroneService.setStatus(res.locals.registration, req.body.status); + res.status(200).json({ success: true }); + } catch (error) { + this.log.error("failed to update drone status", { + _id: res.locals.registration._id.toHexString(), + error, + }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + } + } + + async deleteRegistration(_req: Request, res: Response): Promise { + try { + await DroneService.unregister(res.locals.registration); + res.status(200).json({ success: true }); + } catch (error) { + this.log.error("failed to unregister drone", { error }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + } + } +} + +export default DroneApiControllerV1; diff --git a/gadget-code/src/controllers/api/v1/user.ts b/gadget-code/src/controllers/api/v1/user.ts new file mode 100644 index 0000000..523fd00 --- /dev/null +++ b/gadget-code/src/controllers/api/v1/user.ts @@ -0,0 +1,40 @@ +// src/controllers/api/v1/user.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Request, Response } from "express"; + +import { DtpController } from "../../../lib/controller.js"; + +export class UserApiControllerV1 extends DtpController { + get name(): string { + return "UserApiControllerV1"; + } + get slug(): string { + return "userV1"; + } + get route(): string { + return "/user"; + } + + constructor() { + super(); + } + + async start(): Promise { + this.router.use(this.requireUser()); + + this.router.get("/", this.getUser.bind(this)); + } + + async getUser(req: Request, res: Response): Promise { + res.status(200).json({ + success: true, + data: { + user: req.user, + }, + }); + } +} + +export default UserApiControllerV1; diff --git a/gadget-code/src/controllers/auth.ts b/gadget-code/src/controllers/auth.ts new file mode 100644 index 0000000..97c3ac6 --- /dev/null +++ b/gadget-code/src/controllers/auth.ts @@ -0,0 +1,181 @@ +// src/controllers/auth.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +// import env from "../config/env.js"; +import { NextFunction, Request, Response } from "express"; + +import { DtpController } from "../lib/controller.js"; + +import UserService from "../services/user.js"; +import SessionService, { SessionType } from "../services/session.js"; + +export class AuthController extends DtpController { + get name(): string { + return "AuthController"; + } + get slug(): string { + return "auth"; + } + get route(): string { + return "/auth"; + } + + constructor() { + super(); + } + + async start(): Promise { + this.router.post("/sign-up", this.postSignUp.bind(this)); + this.router.post("/sign-in", this.postSignIn.bind(this)); + this.router.post("/renew-token", this.postRenewToken.bind(this)); + + this.router.get("/welcome", this.getWelcomeView.bind(this)); + this.router.get("/sign-up", this.getSignUpForm.bind(this)); + this.router.get("/sign-in", this.getSignInForm.bind(this)); + this.router.get("/sign-out", this.getSignOut.bind(this)); + } + + async postSignUp( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const user = await UserService.create( + req.body.email, + req.body.password, + req.body.displayName + ); + + req.session.user = { + _id: user._id, + email: user.email, + displayName: user.displayName, + flags: user.flags, + }; + + const token = await SessionService.createJsonWebToken(user); + req.session.token = token; + req.session.type = SessionType.WEB; + + req.session.save((err: Error) => { + if (err) { + return next(err); + } + res.status(201).json({ + success: true, + user: { + _id: user._id.toString(), + email: user.email, + displayName: user.displayName, + flags: user.flags, + }, + token, + }); + }); + } catch (error) { + this.log.error("failed to process new user sign-up", { error }); + return next(error); + } + } + + async postSignIn( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const user = await UserService.authenticate( + req.body.email, + req.body.password + ); + req.session.user = { + _id: user._id, + email: user.email, + displayName: user.displayName, + flags: user.flags, + }; + + const token = await SessionService.createJsonWebToken(user); + req.session.token = token; + req.session.type = SessionType.WEB; + + req.session.save((err: Error) => { + if (err) { + return next(err); + } + res.status(200).json({ + success: true, + user: { + _id: user._id.toString(), + email: user.email, + displayName: user.displayName, + flags: user.flags, + }, + token, + }); + }); + } catch (error) { + return next(error); + } + } + + async postRenewToken(req: Request, res: Response): Promise { + try { + const user = await SessionService.verifyJsonWebToken(req.body.token); + const token = await SessionService.createJsonWebToken(user); + req.session.token = token; + res.status(200).json({ success: true, token }); + } catch (error) { + this.log.error("failed to process token renewal", { error }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + } + } + + async getWelcomeView(_req: Request, res: Response): Promise { + res.status(200).json({ + success: true, + message: "Welcome to DTP Web Application", + }); + } + + async getSignUpForm(_req: Request, res: Response): Promise { + res.status(200).json({ + success: true, + form: "sign-up", + }); + } + async getSignInForm(_req: Request, res: Response): Promise { + res.status(200).json({ + success: true, + form: "sign-in", + }); + } + + async getSignOut( + req: Request, + res: Response, + next: NextFunction + ): Promise { + if (req.session.token) { + try { + await SessionService.revokeJsonWebToken(req.session.token); + } catch (error) { + this.log.error("failed to revoke JSON Web Token", { error }); + } + } + req.session.destroy((err: Error) => { + if (err) { + this.log.error("failed to destroy user session", { error: err }); + return next(err); + } + res.status(200).json({ success: true, message: "Signed out successfully" }); + }); + } +} + +export default AuthController; diff --git a/gadget-code/src/controllers/home.ts b/gadget-code/src/controllers/home.ts new file mode 100644 index 0000000..2944cc7 --- /dev/null +++ b/gadget-code/src/controllers/home.ts @@ -0,0 +1,57 @@ +// src/controllers/home.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { NextFunction, Request, Response } from "express"; + +import { DtpController } from "../lib/controller.js"; + +export class HomeController extends DtpController { + get name(): string { + return "HomeController"; + } + get slug(): string { + return "home"; + } + get route(): string { + return "/"; + } + + constructor() { + super(); + } + + async start(): Promise { + this.router.get("/", this.getHome.bind(this)); + } + + async getHome( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + if (!req.user) { + res.status(200).json({ + success: true, + authenticated: false, + }); + return; + } + res.status(200).json({ + success: true, + authenticated: true, + user: { + _id: req.user._id.toString(), + email: req.user.email, + displayName: req.user.displayName, + }, + }); + } catch (error) { + this.log.error("failed to present the home page", { error }); + return next(error); + } + } +} + +export default HomeController; diff --git a/gadget-code/src/controllers/lib/populators.ts b/gadget-code/src/controllers/lib/populators.ts new file mode 100644 index 0000000..26fa1a5 --- /dev/null +++ b/gadget-code/src/controllers/lib/populators.ts @@ -0,0 +1,75 @@ +// src/controllers/lib/populators.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import assert from "node:assert"; + +import { Types } from "mongoose"; + +import { NextFunction, Request, RequestHandler, Response } from "express"; +import { DtpController } from "../../lib/controller.ts"; + +import DroneService from "../../services/drone.ts"; +import UserService from "../../services/user.ts"; + +export interface PopulateOptions { + requireObject?: boolean; +} + +export function populateUserById( + controller: DtpController, + options?: PopulateOptions, +): RequestHandler { + options = Object.assign({ requireObject: true }, options); + return async function ( + _req: Request, + res: Response, + next: NextFunction, + userId?: string, + ): Promise { + assert(userId, "User ID is required"); + try { + const userIdObj = Types.ObjectId.createFromHexString(userId); + res.locals.userAccount = await UserService.getById(userIdObj); + if (options.requireObject && !res.locals.user) { + const error = new Error("User not found"); + error.statusCode = 404; + throw error; + } + return next(); + } catch (error) { + controller.log.error("failed to populate User by ID", { + userId, + error, + }); + return next(error); + } + }; +} + +export function populateDroneRegistrationById( + controller: DtpController, + options?: PopulateOptions, +): RequestHandler { + options = Object.assign({ requireObject: true }, options); + return async function ( + _req: Request, + res: Response, + next: NextFunction, + registrationId?: string, + ): Promise { + assert(registrationId, "Drone registration ID is required"); + try { + const registrationIdObj = + Types.ObjectId.createFromHexString(registrationId); + res.locals.registration = await DroneService.getById(registrationIdObj); + return next(); + } catch (error) { + controller.log.error("failed to populate User by ID", { + registrationId, + error, + }); + return next(error); + } + }; +} diff --git a/gadget-code/src/controllers/user.ts b/gadget-code/src/controllers/user.ts new file mode 100644 index 0000000..4dbfd30 --- /dev/null +++ b/gadget-code/src/controllers/user.ts @@ -0,0 +1,84 @@ +// src/controllers/user.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { NextFunction, Request, Response } from "express"; + +import UserService from "../services/user.js"; +import { populateUserById } from "./lib/populators.js"; + +import { DtpController } from "../lib/controller.js"; + +export class UserController extends DtpController { + get name(): string { + return "UserController"; + } + get slug(): string { + return "user"; + } + get route(): string { + return "/user"; + } + + constructor() { + super(); + } + + async start(): Promise { + const requireAdmin = this.requireAdmin(); + + this.router.param("userId", populateUserById(this)); + + this.router.post("/:userId", requireAdmin, this.postUserUpdate.bind(this)); + + this.router.get("/:userId", requireAdmin, this.getUserView.bind(this)); + this.router.get("/", requireAdmin, this.getUserHome.bind(this)); + } + + async postUserUpdate( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + await UserService.updateForUser(res.locals.userAccount, req.body); + res.status(200).json({ + success: true, + user: res.locals.userAccount, + }); + } catch (error) { + this.log.error("failed to update user", { error }); + next(error); + } + } + + async getUserView(_req: Request, res: Response): Promise { + res.status(200).json({ + success: true, + message: "User profile view", + }); + } + + async getUserHome( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const pagination = this.getPaginationParameters(req, 20); + + const query = req.query.q ? req.query.q.toString() : undefined; + const recentUsers = await UserService.getRecent(pagination, query); + + res.status(200).json({ + success: true, + users: recentUsers, + }); + } catch (error) { + this.log.error("failed to present User Admin home", { error }); + next(error); + } + } +} + +export default UserController; diff --git a/gadget-code/src/lib/code-session.ts b/gadget-code/src/lib/code-session.ts new file mode 100644 index 0000000..7bdc46f --- /dev/null +++ b/gadget-code/src/lib/code-session.ts @@ -0,0 +1,34 @@ +// src/lib/code-session.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Socket } from "socket.io"; +import { SocketSession, SocketSessionType } from "./socket-session"; +import { IUser } from "@/models/user"; + +export class CodeSession extends SocketSession { + protected type: SocketSessionType = SocketSessionType.Code; + + constructor(socket: Socket, user: IUser) { + super(socket, user); + } + + register() { + super.register(); + + this.socket.on("thinking", this.onThinking.bind(this)); + this.socket.on("response", this.onResponse.bind(this)); + this.socket.on("tool-call", this.onToolCall.bind(this)); + } + + async onThinking(): Promise {} + + async onResponse(): Promise {} + + async onToolCall(): Promise { + this.log.info("tool call received", { + params: { thing: 1 }, + response: "Woooo!", + }); + } +} diff --git a/gadget-code/src/lib/component.ts b/gadget-code/src/lib/component.ts new file mode 100644 index 0000000..6f226b1 --- /dev/null +++ b/gadget-code/src/lib/component.ts @@ -0,0 +1,8 @@ +// src/lib/component.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +export interface DtpComponent { + get name(): string; + get slug(): string; +} diff --git a/gadget-code/src/lib/controller.ts b/gadget-code/src/lib/controller.ts new file mode 100644 index 0000000..39aec88 --- /dev/null +++ b/gadget-code/src/lib/controller.ts @@ -0,0 +1,356 @@ +// src/lib/controller.js +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../config/env.js"; + +import path from "node:path"; +import { v4 as uuidv4 } from "uuid"; +import { rateLimit } from "express-rate-limit"; + +import { Types } from "mongoose"; +import { + Router, + Request, + Response, + NextFunction, + RequestHandler, +} from "express"; + +import multer from "multer"; + +import dayjs from "dayjs"; + +export interface CsrfTokenOptions { + name: string; + expiresMinutes: number; + allowReuse: boolean; +} + +import { ApiClientStatus } from "../models/api-client.js"; +import { CsrfToken, ICsrfToken } from "../models/csrf-token.js"; +import { IUser } from "../models/user.js"; + +import { DtpComponent } from "./component.js"; +import { DtpPaginationParameters } from "./pagination-parameters.js"; +import { DtpLog } from "./log.js"; + +import ApiClientService from "../services/api-client.js"; +import CryptoService from "../services/crypto.js"; + +/** + * The base class for all Web application controllers. A controller binds to an + * HTTP application REST route, and processes requests received on that route. + * + * This is usually accomplished by calling service methods congigured using + * request parameters, headers, and body content, then rendering HTML page or + * JSON object responses. + */ +export abstract class DtpController implements DtpComponent { + log: DtpLog; + router: Router; + + abstract get name(): string; + abstract get slug(): string; + abstract get route(): string; + + constructor() { + this.log = new DtpLog(this); + this.router = Router(); + this.router.use(this.middleware.bind(this)); + } + + abstract start(): Promise; + + /** + * Middleware common to all controllers that populates the view model with + * some commonly expected values. + * @param _req Request The request being processed. + * @param res Response The response being generated. + * @param next NextFunction The next function to call when done. + */ + middleware(req: Request, res: Response, next: NextFunction) { + res.locals.request = req; + res.locals.currentView = this.slug; + res.locals.signupEnabled = env.user.signupEnabled; + next(); + } + + hmacMiddleware() { + return async (req: Request, res: Response, next: NextFunction) => { + if (!req.rawBody) { + this.log.error("No raw body found"); + res.status(401).json({ + success: false, + message: "No raw body found", + }); + return; + } + + const apiClientId = req.headers["x-dtp-client-id"]; + if (!apiClientId) { + this.log.error("API Client ID is required"); + res.status(401).json({ + success: false, + message: "API Client ID is required", + }); + return; + } + + const apiClientIdObj = Types.ObjectId.createFromHexString( + apiClientId as string + ); + const apiClient = await ApiClientService.getById(apiClientIdObj); + if (!apiClient) { + this.log.error("API client not found", { _id: apiClientId }); + res.status(404).json({ + success: false, + message: "API Client not found", + }); + return; + } + + if (apiClient.status !== ApiClientStatus.Active) { + this.log.error("inactive API client", { + _id: apiClientId, + status: apiClient.status, + }); + res.status(403).json({ + success: false, + message: "API client is not active", + }); + return; + } + + const hmacHeader = req.headers["x-dtp-hmac"]; + if (!hmacHeader) { + this.log.error("No HMAC header found"); + res.status(401).json({ + success: false, + message: "No HMAC header found", + }); + return; + } + + const hmacBody = CryptoService.createHmac(apiClient.secret, req.rawBody); + if (hmacHeader !== hmacBody) { + this.log.error("HMAC header does not match body", { + hmacHeader, + hmacBody, + }); + res.status(401).json({ + success: false, + message: "HMAC header does not match body", + }); + return; + } + + this.log.debug("HMAC header matches body"); + + next(); + }; + } + + requireUser(): RequestHandler { + return async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + if (!req.user) { + res.status(403).json({ success: false, message: "Authentication required" }); + return; + } + next(); + }; + } + + requireAdmin(): RequestHandler { + return async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + const user: IUser | null | undefined = req.user; + if (!user || !user.flags.isAdmin) { + res.status(403).json({ success: false, message: "Admin access required" }); + return; + } + next(); + }; + } + + async loadChild(filename: string): Promise { + const pathObj = path.parse(filename); + this.log.info("loading child controller", { + script: pathObj.name, + path: filename, + }); + + const ControllerClass = (await import(filename)).default; + if (!ControllerClass) { + this.log.error( + "failed to receive a default export class from child controller", + { + script: pathObj.name, + } + ); + throw new Error("Child controller failed to provide a default export"); + } + + const controller: DtpController = new ControllerClass(ControllerClass); + + this.log.info("starting child controller", { + name: ControllerClass.name, + }); + await controller.start(); + + const childRoute = this.route + controller.route; + this.log.info("mounting child controller", { + name: ControllerClass.name, + childRoute, + }); + this.router.use(controller.route, controller.router); + + return controller; + } + + /** + * Retrieves set pagination parameters from the request. + * @param req Request The request being processed. + * @param maxPerPage number The maximum number of records to display per page. + * @param pageParamName string The name of the page index parameter. + * @param cppParamName string The name of the count-per-page parameter. + * @returns A new DtpPaginationParameters instance containing pagination + * parameter values. + */ + getPaginationParameters( + req: Request, + maxPerPage: number, + pageParamName: string = "p", + cppParamName: string = "cpp" + ): DtpPaginationParameters { + const pageParam: string = req.query[pageParamName] + ? (req.query[pageParamName] as string) + : "1"; + const cppParam: string = req.query[cppParamName] + ? (req.query[cppParamName] as string) + : maxPerPage.toString(); + const pagination: DtpPaginationParameters = { + p: parseInt(pageParam, 10), + skip: 0, + cpp: parseInt(cppParam, 10), + }; + if (pagination.p < 1) { + pagination.p = 1; + } + if (pagination.cpp > maxPerPage) { + pagination.cpp = maxPerPage; + } + pagination.skip = (pagination.p - 1) * pagination.cpp; + return pagination; + } + + createLimiter( + seconds: number, + limit: number, + message: string, + keyGenerator?: (req: Request, res: Response) => string + ): RequestHandler { + return rateLimit({ + windowMs: seconds * 1000, + limit, + message: message || "Too many requests. Please try again later.", + standardHeaders: "draft-8", + legacyHeaders: false, + statusCode: 429, + keyGenerator, + }); + } + + /** + * Creates a `multipart/form-encoded` HTTP POST processor using the + * [multer](https://www.npmjs.com/package/multer) estension. + * @param slug string The path slug into which files will be stored. + * @param options multer.Options Options for the form processor. + * @returns An ExpressJS middleware that enables a route to receive files. + */ + createMulter(slug: string, options: multer.Options): multer.Multer { + if (!!slug && typeof slug === "object") { + options = slug; + slug = this.slug; + } else { + slug = slug || this.slug; + } + + options = Object.assign( + { + dest: path.join(env.https.uploadPath, slug), + }, + options || {} + ); + + return multer(options); + } + + /** + * Generates a CSRF token used in forms to help prevent XSS attacks. + * @param req Request The request for which a CSRF token will be generated. + * @param options CsrfTokenOptions The options to be used when creating the + * token. + * @returns The new CsrfToken for use. + */ + async createCsrfToken( + req: Request, + options: CsrfTokenOptions + ): Promise { + const NOW = new Date(); + + options = Object.assign( + { + expiresMinutes: 30, + }, + options + ); + + if (options.expiresMinutes > 120) { + const e = new Error("CSRF tokens have a max lifespan of 120 minutes"); + e.statusCode = 400; + throw e; + } + + const token = new CsrfToken(); + token.created = NOW; + token.expires = dayjs(NOW).add(options.expiresMinutes, "minute").toDate(); + if (req.user) { + token.user = req.user._id; + } + if (req.ip) { + token.ip = req.ip; + } + token.token = uuidv4(); + await token.save(); + + const tokenObj = token.toObject(); + tokenObj.name = `csrf-token-${options.name}`; + + return tokenObj; + } + + /** + * Force-save the current user session and ensure it's written before + * proceeding. + * @param req Express.Request The request being processed. + * @returns A promise that resolves when the session is saved. + */ + async saveSession(req: Request): Promise { + return new Promise((resolve, reject) => { + req.session.save((err) => { + if (err) { + return reject(err); + } + resolve(); + }); + }); + } +} diff --git a/gadget-code/src/lib/db.ts b/gadget-code/src/lib/db.ts new file mode 100644 index 0000000..701afd6 --- /dev/null +++ b/gadget-code/src/lib/db.ts @@ -0,0 +1,15 @@ +// src/lib/db.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import mongoose from "mongoose"; + +import { DtpLog } from "./log.js"; +const log = new DtpLog({ name: "db", slug: "db" }); + +const DB_URL = `mongodb://${process.env.DTP_MONGODB_HOST}/${process.env.DTP_MONGODB_DATABASE}`; + +log.info("connecting to MongoDB", DB_URL); +export const db = mongoose.connect(DB_URL); + +export default db; diff --git a/gadget-code/src/lib/drone-session.ts b/gadget-code/src/lib/drone-session.ts new file mode 100644 index 0000000..8e06277 --- /dev/null +++ b/gadget-code/src/lib/drone-session.ts @@ -0,0 +1,22 @@ +// src/lib/drone-session.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { IDroneRegistration } from "@/models/drone-registration"; +import { SocketSession, SocketSessionType } from "./socket-session"; +import { Socket } from "socket.io"; +import { IUser } from "@/models/user"; + +export class DroneSession extends SocketSession { + protected type: SocketSessionType = SocketSessionType.Drone; + registration: IDroneRegistration; + + constructor(socket: Socket, registration: IDroneRegistration) { + super(socket, registration.user as IUser); + this.registration = registration; + } + + register() { + super.register(); + } +} diff --git a/gadget-code/src/lib/log-file.ts b/gadget-code/src/lib/log-file.ts new file mode 100644 index 0000000..66a3587 --- /dev/null +++ b/gadget-code/src/lib/log-file.ts @@ -0,0 +1,60 @@ +// src/lib/log-file.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import fs from "node:fs"; +import path from "node:path"; + +import numeral from "numeral"; + +import { Writable, WritableOptions } from "stream"; + +type StreamCallback = (error?: Error | null) => void; + +export interface DtpLogFileOptions extends WritableOptions { + basePath: string; + name: string; + maxWritesPerFile: number; + maxFiles: number; +} + +export class DtpLogFile extends Writable { + options: DtpLogFileOptions; + file?: fs.WriteStream; + + fileIdx: number = 0; + writeCount: number = 0; + + constructor(options: DtpLogFileOptions) { + super(options); + this.options = options; + } + + open(): void { + fs.mkdirSync(this.options.basePath, { recursive: true }); + const filename = path.join( + this.options.basePath, + `${this.options.name}.${numeral(this.fileIdx).format("000")}.log` + ); + this.file = fs.createWriteStream(filename, { encoding: "utf-8" }); + this.writeCount = 0; + } + + _write( + chunk: unknown, + encoding: BufferEncoding, + callback: StreamCallback + ): boolean { + if (!this.file) { + return false; + } + if (this.writeCount > this.options.maxWritesPerFile) { + this.file.close(); + if (++this.fileIdx > this.options.maxFiles) { + this.fileIdx = 0; + } + this.open(); + } + return this.file.write(chunk, encoding, callback); + } +} diff --git a/gadget-code/src/lib/log-transport-console.ts b/gadget-code/src/lib/log-transport-console.ts new file mode 100644 index 0000000..1229940 --- /dev/null +++ b/gadget-code/src/lib/log-transport-console.ts @@ -0,0 +1,62 @@ +// src/lib/log-transport-console.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import * as util from "node:util"; + +import dayjs from "dayjs"; +import color from "ansicolor"; + +import { DtpLogLevel } from "./log.js"; +import { DtpLogTransport } from "./log-transport.js"; +import { DtpComponent } from "./component.ts"; + +export class DtpLogTransportConsole implements DtpLogTransport { + async writeLog( + timestamp: Date, + component: DtpComponent, + level: DtpLogLevel, + message: string, + metadata?: unknown + ): Promise { + let clevel = level.padEnd(5); + switch (level) { + case "debug": + clevel = color.darkGray(clevel); + break; + case "info": + clevel = color.green(clevel); + break; + case "warn": + clevel = color.yellow(clevel); + break; + case "alert": + clevel = color.red(clevel); + break; + case "error": + clevel = color.bgRed.white(clevel); + break; + case "crit": + clevel = color.bgRed.yellow(clevel); + break; + case "fatal": + clevel = color.bgRed.darkGray(clevel); + break; + } + + const ccomponent = color.cyan(component.name); + const ctimestamp = color.darkGray( + dayjs(timestamp).format("YYYY-MM-DD HH:mm:ss.SSS") + ); + const cmessage = color.darkGray(message); + + if (metadata) { + console.log( + `${ctimestamp} ${clevel} ${ccomponent} ${cmessage}`, + util.inspect(metadata, false, Infinity, true) + ); + } else { + console.log(`${ctimestamp} ${clevel} ${ccomponent} ${cmessage}`); + } + } +} diff --git a/gadget-code/src/lib/log-transport-file.ts b/gadget-code/src/lib/log-transport-file.ts new file mode 100644 index 0000000..1022687 --- /dev/null +++ b/gadget-code/src/lib/log-transport-file.ts @@ -0,0 +1,42 @@ +// src/lib/log-transport-file.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Writable } from "node:stream"; + +import { DtpLogLevel } from "./log.js"; +import { DtpLogTransport } from "./log-transport.js"; +import { DtpComponent } from "./component.ts"; + +export class DtpLogTransportFile implements DtpLogTransport { + file: Writable; + + constructor(file: Writable) { + this.file = file; + } + + async writeLog( + timestamp: Date, + component: DtpComponent, + level: DtpLogLevel, + message: string, + metadata?: unknown + ): Promise { + return new Promise((resolve, reject) => { + const logMessage = JSON.stringify({ + timestamp, + component, + level, + message, + metadata, + }); + const chunk = Buffer.from(logMessage + "\r\n"); + this.file.write(chunk, (error: Error | null | undefined): void => { + if (error) { + return reject(error); + } + return resolve(); + }); + }); + } +} diff --git a/gadget-code/src/lib/log-transport.ts b/gadget-code/src/lib/log-transport.ts new file mode 100644 index 0000000..a7eaee5 --- /dev/null +++ b/gadget-code/src/lib/log-transport.ts @@ -0,0 +1,16 @@ +// src/lib/log-transport.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { DtpComponent } from "./component.ts"; +import { DtpLogLevel } from "./log.js"; + +export abstract class DtpLogTransport { + abstract writeLog( + timestamp: Date, + component: DtpComponent, + level: DtpLogLevel, + message: string, + metadata?: unknown + ): Promise; +} diff --git a/gadget-code/src/lib/log.ts b/gadget-code/src/lib/log.ts new file mode 100644 index 0000000..bb79ab0 --- /dev/null +++ b/gadget-code/src/lib/log.ts @@ -0,0 +1,81 @@ +// src/lib/log.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../config/env.js"; + +import { DtpComponent } from "./component.ts"; +import { DtpLogTransportConsole } from "./log-transport-console.js"; +import { DtpLogTransportFile } from "./log-transport-file.js"; +import { DtpLogTransport } from "./log-transport.js"; +import { DtpLogFile } from "./log-file.js"; + +export enum DtpLogLevel { + debug = "debug", + info = "info", + warn = "warn", + alert = "alert", + error = "error", + crit = "crit", + fatal = "fatal", +} + +export class DtpLog { + component: DtpComponent; + transports: Array = []; + + constructor(component: DtpComponent, file?: DtpLogFile) { + this.component = component; + + if (env.log.console.enabled) { + this.transports.push(new DtpLogTransportConsole()); + } + if (env.log.file.enabled && file) { + this.transports.push(new DtpLogTransportFile(file)); + } + } + + async debug(message: string, metadata?: unknown) { + if (!env.log.levels.debug) { + return; + } + return this.writeLog(DtpLogLevel.debug, message, metadata); + } + + async info(message: string, metadata?: unknown) { + if (!env.log.levels.info) { + return; + } + return this.writeLog(DtpLogLevel.info, message, metadata); + } + + async warn(message: string, metadata?: unknown) { + if (!env.log.levels.warn) { + return; + } + return this.writeLog(DtpLogLevel.warn, message, metadata); + } + + async alert(message: string, metadata?: unknown) { + return this.writeLog(DtpLogLevel.alert, message, metadata); + } + + async error(message: string, metadata?: unknown) { + return this.writeLog(DtpLogLevel.error, message, metadata); + } + + async crit(message: string, metadata?: unknown) { + return this.writeLog(DtpLogLevel.crit, message, metadata); + } + + async fatal(message: string, metadata?: unknown) { + this.writeLog(DtpLogLevel.fatal, message, metadata); + } + + async writeLog(level: DtpLogLevel, message: string, metadata?: unknown) { + const NOW = new Date(); + for (const transport of this.transports) { + transport.writeLog(NOW, this.component, level, message, metadata); + } + } +} diff --git a/gadget-code/src/lib/ollama.ts b/gadget-code/src/lib/ollama.ts new file mode 100644 index 0000000..de4de68 --- /dev/null +++ b/gadget-code/src/lib/ollama.ts @@ -0,0 +1,5 @@ +// src/lib/ollama.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +export { createAiApi } from "@gadget/ai"; \ No newline at end of file diff --git a/gadget-code/src/lib/pagination-parameters.ts b/gadget-code/src/lib/pagination-parameters.ts new file mode 100644 index 0000000..9857261 --- /dev/null +++ b/gadget-code/src/lib/pagination-parameters.ts @@ -0,0 +1,9 @@ +// src/lib/pagination-parameters.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +export class DtpPaginationParameters { + p: number = 1; // page number + cpp: number = 20; // count per page + skip: number = 0; +} diff --git a/gadget-code/src/lib/process.ts b/gadget-code/src/lib/process.ts new file mode 100644 index 0000000..dbc752e --- /dev/null +++ b/gadget-code/src/lib/process.ts @@ -0,0 +1,17 @@ +// src/lib/process.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { DtpComponent } from "./component.js"; +import { DtpLog } from "./log.js"; + +export abstract class DtpProcess implements DtpComponent { + log: DtpLog; + + abstract get name(): string; + abstract get slug(): string; + + constructor() { + this.log = new DtpLog(this); + } +} diff --git a/gadget-code/src/lib/redis.ts b/gadget-code/src/lib/redis.ts new file mode 100644 index 0000000..b05bba2 --- /dev/null +++ b/gadget-code/src/lib/redis.ts @@ -0,0 +1,19 @@ +// src/lib/redis.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../config/env.js"; +import { Redis } from "ioredis"; + +import { DtpLog } from "./log.js"; +const log = new DtpLog({ name: "redis", slug: "redis" }); + +log.info("connecting to Redis", { + host: env.redis.host, + port: env.redis.port, +}); + +const redis = new Redis(env.redis); +redis.setMaxListeners(64); + +export default redis; diff --git a/gadget-code/src/lib/service.ts b/gadget-code/src/lib/service.ts new file mode 100644 index 0000000..aa3c13a --- /dev/null +++ b/gadget-code/src/lib/service.ts @@ -0,0 +1,22 @@ +// src/lib/service.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +// import env from "../config/env.js"; + +import { DtpComponent } from "./component.js"; +import { DtpLog } from "./log.js"; + +export abstract class DtpService implements DtpComponent { + log: DtpLog; + + abstract get name(): string; + abstract get slug(): string; + + constructor() { + this.log = new DtpLog(this); + } + + abstract start(): Promise; + abstract stop(): Promise; +} diff --git a/gadget-code/src/lib/socket-session.ts b/gadget-code/src/lib/socket-session.ts new file mode 100644 index 0000000..24231d7 --- /dev/null +++ b/gadget-code/src/lib/socket-session.ts @@ -0,0 +1,48 @@ +// src/lib/socket-session.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Socket } from "socket.io"; +import { IUser } from "@/models/user"; +import { DtpLog } from "./log"; + +export enum SocketSessionType { + Code = "code", + Drone = "drone", +} + +export abstract class SocketSession { + protected log: DtpLog; + protected socket: Socket; + protected _user: IUser; + + get user() { + return this._user; + } + + protected abstract type: SocketSessionType; + + constructor(socket: Socket, user: IUser) { + this.log = new DtpLog({ + name: "SocketSession", + slug: "lib:socket-session", + }); + this.socket = socket; + this._user = user; + } + + /** + * Registers socket event and message handlers intended to be processed by all + * socket sessions of all types. + */ + register() { + this.socket.on("disconnect", this.onSocketDisconnect.bind(this)); + } + + async onSocketDisconnect(): Promise { + this.log.info("socket disconnected", { + id: this.socket.id, + user: this.user, + }); + } +} diff --git a/gadget-code/src/lib/validators.ts b/gadget-code/src/lib/validators.ts new file mode 100644 index 0000000..6e9d015 --- /dev/null +++ b/gadget-code/src/lib/validators.ts @@ -0,0 +1,42 @@ +// src/lib/validators.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +/** + * Validates if a given string is a valid Firebase user ID. + * + * A valid Firebase user ID must meet the following criteria: + * 1. Must be between 6 and 50 characters long (inclusive) + * 2. Can only contain alphanumeric characters, underscore (_), dot (.), + * hyphen (-), plus (+), and slash (/) characters + * 3. Cannot start or end with a special character + * + * @param {string} userId - The string to validate as a Firebase user ID + * @returns {boolean} True if the string is a valid Firebase user ID, false + * otherwise + */ +export function isValidFirebaseUserId(userId: string): boolean { + // Check length requirements + const minLength = 6; + const maxLength = 50; + + if ( + typeof userId !== "string" || + userId.length < minLength || + userId.length > maxLength + ) { + return false; + } + + // Define allowed special characters + const specialChars = ["_", ".", "-", "+", "/"]; + + // Regular expression to match the Firebase user ID pattern + const regex = new RegExp( + `^[A-Za-z0-9]+(${specialChars + .map((c) => `\\${c}`) + .join("|")}[A-Za-z0-9]*)*$` + ); + + return regex.test(userId); +} diff --git a/gadget-code/src/lib/worker.ts b/gadget-code/src/lib/worker.ts new file mode 100644 index 0000000..a6e4dd4 --- /dev/null +++ b/gadget-code/src/lib/worker.ts @@ -0,0 +1,32 @@ +// src/lib/controller.js +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../config/env.js"; +import BullQueue from "bull"; + +import { DtpLog } from "./log.js"; +import { DtpComponent } from "./component.js"; + +export abstract class DtpWorker implements DtpComponent { + log: DtpLog; + jobQueue: BullQueue.Queue | undefined; + + abstract get name(): string; + abstract get slug(): string; + abstract get queueName(): string; + + constructor() { + this.log = new DtpLog(this); + } + + async start(): Promise { + this.log.info("Starting worker"); + this.log.info("connecting to Bull queue on Redis", { + queueName: this.queueName, + }); + this.jobQueue = new BullQueue(this.queueName, { redis: env.redis }); + } + + abstract stop(): Promise; +} diff --git a/gadget-code/src/models/ai-provider.ts b/gadget-code/src/models/ai-provider.ts new file mode 100644 index 0000000..2b9859e --- /dev/null +++ b/gadget-code/src/models/ai-provider.ts @@ -0,0 +1,124 @@ +// src/models/ai-provider.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Schema, Document, model } from "mongoose"; + +export type AiApiType = "ollama" | "openai"; + +/** + * Normalised capability flags stored with each model record. These are + * populated during model refresh from provider-specific metadata and drive + * slot-based filtering in the UI (e.g. only canCallTools models for Agent). + */ +export interface IAiModelSettings { + temperature?: number; + topP?: number; + topK?: number; + numCtx?: number; +} + +export interface IAiModelCapabilities { + /** Model supports structured function / tool calling via the API. */ + canCallTools: boolean; + /** Model accepts image inputs (multimodal / vision). */ + hasVision: boolean; + /** Model can produce vector embeddings (required for the Vector slot). */ + hasEmbedding: boolean; + /** Model has an explicit reasoning / thinking phase (e.g. o1, QwQ). */ + hasThinking: boolean; + /** Model is instruction-tuned / chat-tuned (as opposed to a base model). */ + isInstructTuned: boolean; +} + +export const AiModelCapabilitiesSchema = new Schema( + { + canCallTools: { type: Boolean, default: false }, + hasVision: { type: Boolean, default: false }, + hasEmbedding: { type: Boolean, default: false }, + hasThinking: { type: Boolean, default: false }, + isInstructTuned: { type: Boolean, default: false }, + }, + { _id: false }, +); + +export interface IAiModel { + id: string; + name: string; + /** + * Raw parameter count in billions (float). Use parameterLabel for display. + */ + parameterCount?: number; + /** + * Human-readable parameter size label sourced directly from the provider, + * e.g. "7b", "70b", "3.8b". + */ + parameterLabel?: string; + contextWindow?: number; + capabilities: IAiModelCapabilities; + settings?: IAiModelSettings; +} + +export const AiModelSettingsSchema = new Schema( + { + temperature: { type: Number }, + topP: { type: Number }, + topK: { type: Number }, + numCtx: { type: Number }, + }, + { _id: false }, +); + +export const AiModelSchema = new Schema( + { + id: { type: String, required: true }, + name: { type: String, required: true }, + parameterCount: { type: Number }, + parameterLabel: { type: String }, + contextWindow: { type: Number }, + capabilities: { + type: AiModelCapabilitiesSchema, + default: () => ({ + canCallTools: false, + hasVision: false, + hasEmbedding: false, + hasThinking: false, + isInstructTuned: false, + }), + }, + settings: { + type: AiModelSettingsSchema, + default: undefined, + }, + }, + { _id: false }, +); + +export interface IAiProvider extends Document { + name: string; + apiType: AiApiType; + baseUrl: string; + apiKey: string; + enabled: boolean; + models: IAiModel[]; + lastModelRefresh: Date; +} + +export const AiProviderSchema = new Schema({ + name: { type: String, required: true }, + apiType: { type: String, enum: ["ollama", "openai"], required: true }, + baseUrl: { type: String, required: true }, + apiKey: { type: String, required: true, select: false }, + enabled: { type: Boolean, default: true, required: true }, + models: { type: [AiModelSchema], default: [], required: true }, + lastModelRefresh: { type: Date, default: Date.now }, +}); + +AiProviderSchema.index({ name: 1 }, { unique: true }); + +export const AiProvider = model("AiProvider", AiProviderSchema); +export default AiProvider; + +// Note: Index synchronization is now handled during application startup +// to ensure the database connection is established first. +// See src/lib/db.ts for the syncDatabaseIndexes function. diff --git a/gadget-code/src/models/api-client-log.ts b/gadget-code/src/models/api-client-log.ts new file mode 100644 index 0000000..b85def3 --- /dev/null +++ b/gadget-code/src/models/api-client-log.ts @@ -0,0 +1,43 @@ +// src/models/api-client-log.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Types, Schema, Document, model } from "mongoose"; + +import { DtpLog } from "../lib/log.js"; +import { IApiClient } from "./api-client.js"; +const log = new DtpLog({ + name: "ApiClientModel", + slug: "apiClient", +}); + +export interface IApiClientLog extends Document { + _id: Types.ObjectId; + client: IApiClient | Types.ObjectId; + createdAt: Date; + method: string; + url: string; +} +export const ApiClientLogSchema = new Schema({ + client: { type: Types.ObjectId, required: true, index: 1, ref: "ApiClient" }, + createdAt: { + type: Date, + default: Date.now, + required: true, + index: -1, + expires: "30d", + }, + method: { type: String, required: true }, + url: { type: String, required: true }, +}); + +export const ApiClientLog = model( + "ApiClientLog", + ApiClientLogSchema +); +export default ApiClientLog; + +(async () => { + log.info("Syncing indexes..."); + await ApiClientLog.syncIndexes(); +})(); diff --git a/gadget-code/src/models/api-client.ts b/gadget-code/src/models/api-client.ts new file mode 100644 index 0000000..8db3c54 --- /dev/null +++ b/gadget-code/src/models/api-client.ts @@ -0,0 +1,49 @@ +// src/models/api-client.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Types, Schema, Document, model } from "mongoose"; + +import { DtpLog } from "../lib/log.js"; +const log = new DtpLog({ + name: "ApiClientModel", + slug: "apiClient", +}); + +export enum ApiClientStatus { + Active = "active", + Inactive = "inactive", + Archived = "archived", +} + +export interface IApiClient extends Document { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; + status: ApiClientStatus; + name: string; + description?: string; + secret: string; +} +const ApiClientSchema = new Schema({ + createdAt: { type: Date, required: true }, + updatedAt: { type: Date, required: true }, + status: { + type: String, + enum: ApiClientStatus, + default: ApiClientStatus.Active, + required: true, + index: 1, + }, + name: { type: String, required: true }, + description: { type: String }, + secret: { type: String, required: true }, +}); + +export const ApiClient = model("ApiClient", ApiClientSchema); +export default ApiClient; + +(async () => { + log.info("Syncing indexes..."); + await ApiClient.syncIndexes(); +})(); diff --git a/gadget-code/src/models/chat-session.ts b/gadget-code/src/models/chat-session.ts new file mode 100644 index 0000000..2f067e7 --- /dev/null +++ b/gadget-code/src/models/chat-session.ts @@ -0,0 +1,66 @@ +// src/models/chat-session.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Types, Schema, Document, model } from "mongoose"; + +import { IUser } from "./user.js"; +import { IProject } from "./project.js"; + +export enum ChatSessionMode { + Plan = "plan", // for planning and brainstorming + Build = "build", // for building and coding + Test = "test", // for testing and debugging + Ship = "ship", // for finalizing and shipping + Develop = "dev", // for working on the Gadget Code harness itself +} + +export interface IChatSessionPin { + _id?: Types.ObjectId; + content: string; +} +export const ChatSessionPinSchema = new Schema({ + content: { type: String, required: true }, +}); + +export interface IChatSession extends Document { + createdAt: Date; + lastMessageAt?: Date; + user: IUser | Types.ObjectId; + project: IProject | Types.ObjectId; + name: string; + mode: ChatSessionMode; + stats: { + turnCount: number; + toolCallCount: number; + inputTokens: number; + outputTokens: number; + }; + pins: IChatSessionPin[]; +} +export const ChatSessionSchema = new Schema({ + createdAt: { type: Date, default: Date.now, required: true }, + lastMessageAt: { type: Date }, + user: { type: Types.ObjectId, required: true, index: 1, ref: "User" }, + project: { type: Types.ObjectId, required: false, index: 1, ref: "Project" }, + name: { type: String, default: "New Session", required: true }, + mode: { + type: String, + enum: ChatSessionMode, + default: ChatSessionMode.Build, + required: true, + }, + stats: { + turnCount: { type: Number, default: 0, required: true }, + toolCallCount: { type: Number, default: 0, required: true }, + inputTokens: { type: Number, default: 0, required: true }, + outputTokens: { type: Number, default: 0, required: true }, + }, + pins: { type: [ChatSessionPinSchema], default: [], required: true }, +}); + +export const ChatSession = model( + "ChatSession", + ChatSessionSchema, +); +export default ChatSession; diff --git a/gadget-code/src/models/chat-turn.ts b/gadget-code/src/models/chat-turn.ts new file mode 100644 index 0000000..f01768e --- /dev/null +++ b/gadget-code/src/models/chat-turn.ts @@ -0,0 +1,146 @@ +// src/models/chat-turn.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Types, Schema, Document, model } from "mongoose"; + +import { IUser } from "./user.js"; +import { IProject } from "./project.js"; +import { ChatSessionMode, IChatSession } from "./chat-session.js"; +import { IAiProvider } from "./ai-provider.js"; + +export enum ChatTurnStatus { + Processing = "processing", + Finished = "finished", + Error = "error", +} + +export interface IChatTurnStats { + toolCallCount: number; // total number of tool functions called this turn + inputTokens: number; // total number of input tokens processed this turn + thinkingTokenCount: number; // total number of thinking tokens generated this turn + responseTokens: number; // total number of response/output tokens generated this turn + durationMs: number; // total turn runtime in seconds + durationLabel: string; // total turn runtime as hh:mm:ss +} +export const ChatTurnStatsSchema = new Schema({ + toolCallCount: { type: Number, default: 0, required: true }, + inputTokens: { type: Number, default: 0, required: true }, + thinkingTokenCount: { type: Number, default: 0, required: true }, + responseTokens: { type: Number, default: 0, required: true }, + durationMs: { type: Number, default: 0, required: true }, + durationLabel: { type: String, default: "pending", required: true }, +}); + +export interface IChatToolCall { + name: string; // tool function name being called + parameters: string; // JSON.stringify of input parameters + response: string; // the tool's response +} +export const ChatToolCallSchema = new Schema({ + name: { type: String, required: true }, + parameters: { type: String, required: false }, + response: { type: String, required: false }, +}); + +export interface IChatSubagentProcess { + prompt: string; + thinking?: string; + response: string; + toolCalls: IChatToolCall[]; + stats: IChatTurnStats; +} +export const ChatSubagentProcessSchema = new Schema({ + prompt: { type: String, required: true }, + thinking: { type: String, required: false }, + response: { type: String, required: false }, + toolCalls: { type: [ChatToolCallSchema], default: [], required: true }, + stats: { type: ChatTurnStatsSchema, default: {}, required: true }, +}); + +/** + * A chat turn is a single prompt/response pair with tool call accounting. It + * stores all data generated by one run of the Agentic Workflow Loop by a Gadget + * Drone process. + */ +export interface IChatTurn extends Document { + _id: Types.ObjectId; + createdAt: Date; + user: IUser | Types.ObjectId; + project: IProject | Types.ObjectId; + session: IChatSession | Types.ObjectId; + provider: IAiProvider | Types.ObjectId; + llm: string; // id/name of the model used to process the prompt + mode: ChatSessionMode; // session mode for this turn/prompt + status: ChatTurnStatus; + prompt: string; + thinking?: string; + response?: string; + toolCalls: IChatToolCall[]; + subagents: IChatSubagentProcess[]; // subagents used while processing this turn + stats: IChatTurnStats; +} +export const ChatTurnSchema = new Schema({ + createdAt: { type: Date, default: Date.now, required: true }, + user: { type: Types.ObjectId, required: true, ref: "User" }, + project: { type: Types.ObjectId, required: false, ref: "Project" }, + session: { type: Types.ObjectId, required: true, ref: "ChatSession" }, + provider: { type: Types.ObjectId, required: true, ref: "AiProvider" }, + llm: { type: String, required: true }, // id/name of the model used to process the prompt + mode: { + type: String, + enum: ChatSessionMode, + default: ChatSessionMode.Build, + required: true, + }, + status: { + type: String, + enum: ChatTurnStatus, + default: ChatTurnStatus.Processing, + required: true, + }, + prompt: { type: String, required: true }, + thinking: { type: String, required: false }, + response: { type: String, required: false }, + toolCalls: { type: [ChatToolCallSchema], default: [], required: true }, + subagents: { type: [ChatSubagentProcessSchema], default: [], required: true }, + stats: { + toolCallCount: { type: Number, default: 0, required: true }, + inputTokens: { type: Number, default: 0, required: true }, + thinkingTokens: { type: Number, default: 0, required: true }, + responseTokens: { type: Number, default: 0, required: true }, + durationMs: { type: Number, default: 0, required: true }, + durationLabel: { type: String, default: "pending", required: true }, + }, +}); + +ChatTurnSchema.index( + { + user: 1, + project: 1, + session: 1, + }, + { + name: "chat-turn-user-project-session-index", + }, +); + +ChatTurnSchema.index( + { + user: 1, + prompt: "text", + thinking: "text", + response: "text", + }, + { + weights: { + prompt: 10, + response: 5, + thinking: 1, + }, + name: "chat-turn-user-text-index", + }, +); + +export const ChatTurn = model("ChatTurn", ChatTurnSchema); +export default ChatTurn; diff --git a/gadget-code/src/models/csrf-token.ts b/gadget-code/src/models/csrf-token.ts new file mode 100644 index 0000000..3e771a8 --- /dev/null +++ b/gadget-code/src/models/csrf-token.ts @@ -0,0 +1,50 @@ +// src/models/csrf-token.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Schema, Types, model } from "mongoose"; +import { IUser } from "./user.js"; + +import { DtpLog } from "../lib/log.js"; +const log = new DtpLog({ + name: "CsrfTokenModel", + slug: "csrfToken", +}); + +export interface ICsrfToken { + _id: Types.ObjectId; + created: Date; + expires: Date; + claimed?: Date; + token: string; + user?: IUser | Types.ObjectId; + ip: string; + + /* + * not saved + */ + name?: string; +} + +const CsrfTokenSchema = new Schema({ + created: { + type: Date, + required: true, + default: Date.now, + index: -1, + expires: "72h", + }, + expires: { type: Date, required: true, default: Date.now, index: -1 }, + claimed: { type: Date }, + token: { type: String, required: true, index: 1 }, + user: { type: Types.ObjectId, ref: "User" }, + ip: { type: String, required: true }, +}); + +export const CsrfToken = model("CsrfToken", CsrfTokenSchema); +export default CsrfToken; + +(async () => { + log.info("Syncing indexes..."); + await CsrfToken.syncIndexes(); +})(); diff --git a/gadget-code/src/models/drone-monitor.ts b/gadget-code/src/models/drone-monitor.ts new file mode 100644 index 0000000..4601e4b --- /dev/null +++ b/gadget-code/src/models/drone-monitor.ts @@ -0,0 +1,77 @@ +// src/models/drone-monitor.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Types, Schema, Document, model } from "mongoose"; +import { IDroneRegistration } from "./drone-registration"; + +/* + * Memory Monitor interface & schema + */ +export interface IMemoryMonitor { + count: number; + bytes: number; +} +export const MemoryMonitorSchema = new Schema({ + count: { type: Number, default: 0, required: true }, + bytes: { type: Number, default: 0, required: true }, +}); + +/* + * DroneMonitor interface & schema + */ +export interface IDroneMonitor extends Document { + _id: Types.ObjectId; + registration: IDroneRegistration | Types.ObjectId; + timestamp: Date; + memory: { + rss: number; + v8: { + heapTotal: number; + heapUsed: number; + heapExternal: number; + }; + os: { + total: number; + free: number; + }; + ai: { + subagents: IMemoryMonitor; + fileOperations: IMemoryMonitor; + toolCalls: IMemoryMonitor; + }; + logs: IMemoryMonitor; + }; +} +export const DroneMonitorSchema = new Schema({ + registration: { type: Types.ObjectId, required: true, index: 1 }, + timestamp: { type: Date, required: true, index: -1 }, + memory: { + rss: { type: Number, required: true }, + v8: { + heapTotal: { type: Number, required: true }, + heapUsed: { type: Number, required: true }, + heapExternal: { type: Number, required: true, default: 0 }, + }, + os: { + total: { type: Number, required: true }, + free: { type: Number, required: true }, + }, + ai: { + subagents: { type: MemoryMonitorSchema, required: true }, + fileOperations: { type: MemoryMonitorSchema, required: true }, + toolCalls: { type: MemoryMonitorSchema, required: true }, + }, + logs: { type: MemoryMonitorSchema, required: true }, + }, +}); + +export const DroneMonitor = model( + "DroneMonitor", + DroneMonitorSchema, +); +export default DroneMonitor; + +(async () => { + await DroneMonitor.syncIndexes(); +})(); diff --git a/gadget-code/src/models/drone-registration.ts b/gadget-code/src/models/drone-registration.ts new file mode 100644 index 0000000..60ea221 --- /dev/null +++ b/gadget-code/src/models/drone-registration.ts @@ -0,0 +1,52 @@ +// src/models/drone-registration.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Document, Schema, Types, model } from "mongoose"; +import { IUser } from "./user"; + +export enum DroneStatus { + Starting = "starting", + Available = "available", + Busy = "busy", + Offline = "offline", +} + +export interface IDroneRegistration extends Document { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; + user: IUser | Types.ObjectId; + hostname: string; + status: DroneStatus; + currentJobId?: string; +} + +export const DroneRegistrationSchema = new Schema({ + createdAt: { type: Date, required: true }, + updatedAt: { type: Date, required: false }, + user: { type: Schema.Types.ObjectId, ref: "User", required: true }, + hostname: { type: String, required: true }, + status: { + type: String, + enum: DroneStatus, + default: DroneStatus.Starting, + required: true, + }, + currentJobId: { type: String, required: false }, +}); + +DroneRegistrationSchema.index({ + user: 1, + status: 1, +}); + +export const DroneRegistration = model( + "DroneRegistration", + DroneRegistrationSchema, +); +export default DroneRegistration; + +(async () => { + await DroneRegistration.syncIndexes(); +})(); diff --git a/gadget-code/src/models/email-log.ts b/gadget-code/src/models/email-log.ts new file mode 100644 index 0000000..31d459e --- /dev/null +++ b/gadget-code/src/models/email-log.ts @@ -0,0 +1,37 @@ +// src/models/email-log.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Schema, Types, Document, model } from "mongoose"; + +import { DtpLog } from "../lib/log.js"; +const log = new DtpLog({ + name: "EmailLogModel", + slug: "emailLog", +}); + +export interface IEmailLog extends Document { + _id: Types.ObjectId; + created: Date; + from: string; + to: string; + to_lc: string; + subject: string; + messageId: string; +} +export const EmailLogSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: -1 }, + from: { type: String, required: true }, + to: { type: String, required: true }, + to_lc: { type: String, required: true, lowercase: true, index: 1 }, + subject: { type: String, required: true }, + messageId: { type: String }, +}); + +export const EmailLog = model("EmailLog", EmailLogSchema); +export default EmailLog; + +(async () => { + log.info("Syncing indexes..."); + await EmailLog.syncIndexes(); +})(); diff --git a/gadget-code/src/models/email-verification.ts b/gadget-code/src/models/email-verification.ts new file mode 100644 index 0000000..40c0a4d --- /dev/null +++ b/gadget-code/src/models/email-verification.ts @@ -0,0 +1,47 @@ +// src/models/email-verification.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Schema, Types, model } from "mongoose"; + +import { DtpLog } from "../lib/log.js"; +import { IUser } from "./user.js"; +const log = new DtpLog({ + name: "EmailVerificationModel", + slug: "emailVerification", +}); + +export enum EmailVerificationStatus { + Pending = "pending", + Verified = "verified", + Expired = "expired", +} + +export interface IEmailVerification { + createdAt: Date; + user: IUser | Types.ObjectId; + code: string; + status: EmailVerificationStatus; +} +export const EmailVerificationSchema = new Schema({ + createdAt: { type: Date, required: true, default: Date.now }, + user: { type: Schema.Types.ObjectId, required: true, index: 1, ref: "User" }, + code: { type: String, required: true, unique: true }, + status: { + type: String, + enum: EmailVerificationStatus, + required: true, + default: EmailVerificationStatus.Pending, + }, +}); + +export const EmailVerification = model( + "EmailVerification", + EmailVerificationSchema +); +export default EmailVerification; + +(async () => { + log.info("Syncing indexes..."); + await EmailVerification.syncIndexes(); +})(); diff --git a/gadget-code/src/models/postal-code.ts b/gadget-code/src/models/postal-code.ts new file mode 100644 index 0000000..70cc978 --- /dev/null +++ b/gadget-code/src/models/postal-code.ts @@ -0,0 +1,48 @@ +import { Schema, model } from "mongoose"; + +import { DtpLog } from "../lib/log.js"; +const log = new DtpLog({ name: "PostalCodeModel", slug: "postalCode" }); + +/* + * This model provides storage for the Postal Code database found here: + * https://data.opendatasoft.com/explore/dataset/geonames-postal-code@public/export/?flg=en-us + * + * When uniqued by country+postalCode, there are many dupes. So the ingest + * routine filters them out. + */ + +export interface IPostalCode { + country: string; // "country code" + postalCode: string; // "postal code" + city: string; // "place name" + state: string; // "admin name1" + stateCode: string; // "admin code1" + county: string; // "admin name2" +} +export const PostalCodeSchema = new Schema({ + country: { type: String, required: true }, + postalCode: { type: String, required: true }, + city: { type: String }, + state: { type: String }, + stateCode: { type: String }, + county: { type: String }, +}); + +PostalCodeSchema.index( + { + country: 1, + postalCode: 1, + }, + { + unique: true, + name: "postal_code_unique", + } +); + +export const PostalCode = model("PostalCode", PostalCodeSchema); +export default PostalCode; + +(async () => { + log.info("Syncing indexes..."); + await PostalCode.syncIndexes(); +})(); diff --git a/gadget-code/src/models/project.ts b/gadget-code/src/models/project.ts new file mode 100644 index 0000000..513d72e --- /dev/null +++ b/gadget-code/src/models/project.ts @@ -0,0 +1,53 @@ +// src/models/project.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Types, Schema, Document, model } from "mongoose"; +import { IUser } from "./user.js"; + +export interface IProject extends Document { + createdAt: Date; + user: IUser | Types.ObjectId; + name: string; + slug: string; + gitUrl?: string; +} + +export const ProjectSchema = new Schema({ + createdAt: { type: Date, default: Date.now, required: true }, + user: { type: Types.ObjectId, required: true, index: 1, ref: "User" }, + name: { type: String, default: "New Project", required: true }, + slug: { + type: String, + default: "new-project", + lowercase: true, + required: true, + }, + gitUrl: { type: String }, +}); + +ProjectSchema.index( + { + user: 1, + slug: 1, + }, + { + unique: true, + }, +); + +ProjectSchema.index( + { + user: 1, + gitUrl: 1, + }, + { + partialFilterExpression: { + gitUrl: { $exists: true }, + }, + unique: true, + }, +); + +export const Project = model("Project", ProjectSchema); +export default Project; diff --git a/gadget-code/src/models/user.ts b/gadget-code/src/models/user.ts new file mode 100644 index 0000000..1982ec6 --- /dev/null +++ b/gadget-code/src/models/user.ts @@ -0,0 +1,53 @@ +// src/models/user.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Types, Schema, Document, model } from "mongoose"; + +import { DtpLog } from "../lib/log.js"; +const log = new DtpLog({ + name: "UserModel", + slug: "user", +}); + +export interface IUserFlags { + isEmailVerified: boolean; + isAdmin: boolean; + isTest: boolean; + isBanned: boolean; +} +export const UserFlagsSchema = new Schema( + { + isEmailVerified: { type: Boolean, default: false, required: true }, + isAdmin: { type: Boolean, default: false, required: true }, + isTest: { type: Boolean, default: false, required: true }, + isBanned: { type: Boolean, default: false, required: true }, + }, + { _id: false } +); + +export interface IUser extends Document { + _id: Types.ObjectId; + email: string; + email_lc: string; + passwordSalt?: string; + password?: string; + displayName: string; + flags: IUserFlags; +} +export const UserSchema = new Schema({ + email: { type: String, required: true }, + email_lc: { type: String, required: true, lowercase: true, unique: true }, + passwordSalt: { type: String, required: true, select: false }, + password: { type: String, required: true, select: false }, + displayName: { type: String, minlength: 3, maxlength: 30, required: true }, + flags: { type: UserFlagsSchema, required: true }, +}); + +export const User = model("User", UserSchema); +export default User; + +(async () => { + log.info("Syncing indexes..."); + await User.syncIndexes(); +})(); diff --git a/gadget-code/src/models/web-token.ts b/gadget-code/src/models/web-token.ts new file mode 100644 index 0000000..067acde --- /dev/null +++ b/gadget-code/src/models/web-token.ts @@ -0,0 +1,34 @@ +// app/models/web-token.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Schema, Types, model } from "mongoose"; +import { IUser } from "./user.js"; + +import { DtpLog } from "../lib/log.js"; +const log = new DtpLog({ + name: "WebTokenModel", + slug: "webToken", +}); + +export interface IWebToken { + _id: Types.ObjectId; + created: Date; + expires: Date; + user: IUser | Types.ObjectId; + token: string; +} +export const WebTokenSchema = new Schema({ + created: { type: Date, required: true, index: 1 }, + expires: { type: Date, required: true, index: -1 }, + user: { type: Schema.Types.ObjectId, required: true, ref: "User" }, + token: { type: String, required: true }, +}); + +export const WebToken = model("WebToken", WebTokenSchema); +export default WebToken; + +(async () => { + log.info("Syncing indexes..."); + await WebToken.syncIndexes(); +})(); diff --git a/gadget-code/src/models/web-visit.ts b/gadget-code/src/models/web-visit.ts new file mode 100644 index 0000000..234d044 --- /dev/null +++ b/gadget-code/src/models/web-visit.ts @@ -0,0 +1,52 @@ +// src/models/web-visit.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { Schema, Types, Document, model } from "mongoose"; + +import { DtpLog } from "../lib/log.js"; +import { IUser } from "./user.js"; +const log = new DtpLog({ + name: "WebVisitModel", + slug: "webVisit", +}); + +export interface IWebVisit extends Document { + _id: Types.ObjectId; + created: Date; + url: string; + user?: IUser | Types.ObjectId; + userAgent?: string; + referrer?: string; + ipAddress?: string; + country?: string; + region?: string; + eu: boolean; + timezone?: string; + city?: string; + latitude?: number; + longitude?: number; + metroCode?: number; +} +export const WebVisitSchema = new Schema({ + created: { type: Date, default: Date.now }, + url: { type: String, required: true }, + user: { type: Types.ObjectId, index: 1, ref: "User" }, + userAgent: { type: String }, + referrer: { type: String }, + ipAddress: { type: String }, + country: { type: String }, + region: { type: String }, + eu: { type: Boolean, default: false }, + timezone: { type: String }, + city: { type: String }, + latitude: { type: Number }, + longitude: { type: Number }, + metroCode: { type: Number }, +}); +export const WebVisit = model("WebVisit", WebVisitSchema); + +(async () => { + log.info("Syncing indexes..."); + await WebVisit.syncIndexes(); +})(); diff --git a/gadget-code/src/services/api-client.ts b/gadget-code/src/services/api-client.ts new file mode 100644 index 0000000..a6f2482 --- /dev/null +++ b/gadget-code/src/services/api-client.ts @@ -0,0 +1,108 @@ +// services/api-client.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +// import env, { getCountryName } from "../config/env.js"; +import assert from "node:assert"; + +import { + Types, + // MongooseQueryOptions, + // MongooseUpdateQueryOptions, +} from "mongoose"; +import { Request } from "express"; + +import { filterText } from "dtp-cleantext"; +import { v4 as uuidv4 } from "uuid"; + +import ApiClient, { + ApiClientStatus, + IApiClient, +} from "../models/api-client.js"; +import ApiClientLog, { IApiClientLog } from "../models/api-client-log.js"; + +import { DtpService } from "../lib/service.js"; + +class ApiClientService extends DtpService { + get name(): string { + return "ApiClientService"; + } + get slug(): string { + return "apiClient"; + } + + constructor() { + super(); + } + + async start(): Promise {} + + async stop(): Promise {} + + async create(definition: Partial): Promise { + const NOW = new Date(); + const apiClient = new ApiClient(); + apiClient.createdAt = NOW; + apiClient.updatedAt = NOW; + apiClient.status = ApiClientStatus.Active; + + assert(definition.name, "ApiClient name is required"); + apiClient.name = filterText(definition.name); + + if (definition.description) { + apiClient.description = filterText(definition.description); + } + + apiClient.secret = uuidv4(); + + await apiClient.save(); + + return apiClient.toObject(); + } + + async setStatus( + client: IApiClient, + status: ApiClientStatus, + ): Promise { + this.log.info("updating ApiClient status", { _id: client._id, status }); + + const newClient = await ApiClient.findOneAndUpdate( + { _id: client._id }, + { $set: { status } }, + { new: true }, + ); + if (!newClient) { + const error = new Error("ApiClient not found"); + error.statusCode = 404; + throw error; + } + + return newClient; + } + + async getById(clientId: Types.ObjectId): Promise { + const client = await ApiClient.findOne({ _id: clientId }); + return client; + } + + async logRequest(client: IApiClient, req: Request): Promise { + const NOW = new Date(); + const log = new ApiClientLog(); + + log.client = client._id; + log.createdAt = NOW; + log.method = req.method; + log.url = req.url; + + await log.save(); + + return log.toObject(); + } + + async remove(client: IApiClient): Promise { + this.log.info("removing API client", { _id: client._id }); + await ApiClient.deleteOne({ _id: client._id }); + } +} + +export default new ApiClientService(); diff --git a/gadget-code/src/services/contact.ts b/gadget-code/src/services/contact.ts new file mode 100644 index 0000000..2a6d3bf --- /dev/null +++ b/gadget-code/src/services/contact.ts @@ -0,0 +1,178 @@ +// src/services/contact.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../config/env.js"; +// import assert from "node:assert"; +import crypto from "node:crypto"; + +import nodemailer from "nodemailer"; +import Mail from "nodemailer/lib/mailer"; + +import EmailLog, { IEmailLog } from "../models/email-log.js"; +import EmailVerification, { + EmailVerificationStatus, + IEmailVerification, +} from "../models/email-verification.js"; +import { IUser } from "../models/user.js"; + +import UserService from "./user.js"; +import { DtpService } from "../lib/service.js"; +import { PopulateOptions } from "mongoose"; + +export type EmailAddress = Mail.Address | string; + +export interface EmailMessage { + from: EmailAddress; + replyTo?: EmailAddress; + to: EmailAddress; + cc?: EmailAddress; + subject: string; + text?: string; + html?: string; + attachments?: Array; +} + +class ContactService extends DtpService { + private transport?: nodemailer.Transporter; + private populateEmailVerification: PopulateOptions[] = []; + + get name(): string { + return "ContactService"; + } + get slug(): string { + return "contact"; + } + + async start(): Promise { + this.log.info("creating SMTP transport", { + host: env.email.smtp.host, + port: env.email.smtp.port, + }); + const transportOptions = { + host: env.email.smtp.host, + port: env.email.smtp.port, + secure: env.email.smtp.secure, + auth: { + user: env.email.smtp.user, + pass: env.email.smtp.password, + }, + pool: env.email.smtp.pool.enabled, + maxConnections: env.email.smtp.pool.maxConnections, + maxMessages: env.email.smtp.pool.maxMessages, + }; + this.transport = nodemailer.createTransport(transportOptions); + + this.populateEmailVerification = [ + { + path: "user", + }, + ]; + } + + async stop(): Promise {} + + async sendEmail(message: EmailMessage): Promise { + if (!this.transport) { + const error = new Error("Email transport required"); + error.statusCode = 503; + throw error; + } + + const NOW = new Date(); + + this.log.info("sending email", { to: message.to }); + const response = await this.transport.sendMail(message); + this.log.debug("email sent", { to: message.to, response }); + + const from = + typeof message.from === "object" ? message.from.address : message.from; + const to = typeof message.to === "object" ? message.to.address : message.to; + const log = await EmailLog.create({ + created: NOW, + from, + to, + to_lc: to.toLowerCase(), + subject: message.subject, + messageId: response.messageId, + }); + + return log.toObject(); + } + + sendVerificationEmail(user: IUser): Promise { + const NOW = new Date(); + + const verification = new EmailVerification(); + verification.createdAt = NOW; + verification.code = crypto.randomBytes(20).toString("hex").toUpperCase(); + verification.status = EmailVerificationStatus.Pending; + verification.user = user._id; + + const verificationLink = `https://${env.site.host}/auth/email-verify?code=${verification.code}`; + const message: EmailMessage = { + from: env.email.contact.to, + to: user.email, + subject: `Please verify your email address (${env.site.name})`, + text: `Hello ${user.displayName}, + +Please verify your email address for use at ${env.site.name} by clicking the link below: + +${verificationLink} + +If you did not create an account with us, please ignore this email. + +Thank you, +The ${env.site.name} Team +`, + }; + + return this.sendEmail(message); + } + + async verifyEmailCode(code: string): Promise { + const verification = await EmailVerification.findOne({ code }).lean(); + if (!verification) { + const error = new Error("Invalid verification code"); + error.statusCode = 400; + throw error; + } + + if (verification.status !== EmailVerificationStatus.Pending) { + const error = new Error( + `Verification code is not pending (status: ${verification.status})` + ); + error.statusCode = 400; + throw error; + } + + const NOW = new Date(); + const elapsedMs = NOW.getTime() - verification.createdAt.getTime(); + const elapsedHours = elapsedMs / (1000 * 60 * 60); + if (elapsedHours > 24) { + const error = new Error("Verification code has expired"); + error.statusCode = 400; + throw error; + } + + verification.status = EmailVerificationStatus.Verified; + const newVerification = await EmailVerification.findOneAndUpdate( + { _id: verification._id }, + { + status: EmailVerificationStatus.Verified, + }, + { new: true, populate: this.populateEmailVerification, lean: true } + ); + if (!newVerification) { + const error = new Error("Failed to update verification status"); + error.statusCode = 500; + throw error; + } + + await UserService.setEmailVerified(verification.user as IUser, true); + + return newVerification; + } +} + +export default new ContactService(); diff --git a/gadget-code/src/services/crypto.ts b/gadget-code/src/services/crypto.ts new file mode 100644 index 0000000..8d62b61 --- /dev/null +++ b/gadget-code/src/services/crypto.ts @@ -0,0 +1,50 @@ +// src/services/crypto.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { DtpService } from "../lib/service.js"; +import env from "../config/env.js"; +import assert from "node:assert"; + +import crypto from "node:crypto"; + +class CryptoService extends DtpService { + get name(): string { + return "CryptoService"; + } + get slug(): string { + return "crypto"; + } + + async start(): Promise {} + async stop(): Promise {} + + async maskPassword(passwordSalt: string, password: string): Promise { + assert(env.user.passwordSalt, "Password salt is required"); + const hash = crypto.createHash("sha256"); + + hash.update(env.user.passwordSalt); // environment + hash.update(passwordSalt); // per-user + hash.update(password); + + return hash.digest("hex"); + } + + createHash(content: string | Buffer, algorithm: string = "sha256"): string { + const hash = crypto.createHash(algorithm); + hash.update(content); + return hash.digest("hex"); + } + + createHmac( + secret: string, + content: string | Buffer, + algorithm: string = "sha256" + ): string { + const hmac = crypto.createHmac(algorithm, secret); + hmac.update(content); + return hmac.digest("hex"); + } +} + +export default new CryptoService(); diff --git a/gadget-code/src/services/drone.ts b/gadget-code/src/services/drone.ts new file mode 100644 index 0000000..576be90 --- /dev/null +++ b/gadget-code/src/services/drone.ts @@ -0,0 +1,107 @@ +// services/drone.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import DroneRegistration, { + DroneStatus, + IDroneRegistration, +} from "@/models/drone-registration.js"; +import { DtpService } from "../lib/service.js"; +import { IUser } from "@/models/user.js"; +import { PopulateOptions, Types } from "mongoose"; + +export interface IDroneDefinition { + hostname: string; +} + +class DroneService extends DtpService { + private populateDroneRegistration: PopulateOptions[]; + + constructor() { + super(); + this.populateDroneRegistration = [ + { + path: "user", + select: "-passwordSalt -password", + }, + ]; + } + + get name(): string { + return "DroneService"; + } + + get slug(): string { + return "svc:drone"; + } + + async start(): Promise { + this.log.info("service started"); + } + + async stop(): Promise { + this.log.info("service stopped"); + } + + async register( + user: IUser, + definition: IDroneDefinition, + ): Promise { + const NOW = new Date(); + let registration = new DroneRegistration(); + + registration.createdAt = NOW; + registration.updatedAt = NOW; + registration.user = user._id; + registration.hostname = definition.hostname; + registration.status = DroneStatus.Starting; + + this.log.info("registering drone", { hostname: registration.hostname }); + await registration.save(); + + return registration.populate(this.populateDroneRegistration); + } + + async unregister( + registration: IDroneRegistration, + ): Promise { + this.log.info("unregistering drone", { hostname: registration.hostname }); + return this.setStatus(registration, DroneStatus.Offline); + } + + async getById(registrationId: Types.ObjectId): Promise { + const registration = await DroneRegistration.findById( + registrationId, + ).populate(this.populateDroneRegistration); + if (!registration) { + const error = new Error("drone registration not found"); + error.statusCode = 404; + throw error; + } + return registration; + } + + async setStatus( + registration: IDroneRegistration, + status: DroneStatus, + ): Promise { + this.log.info("setting drone status", { + hostname: registration.hostname, + old: registration.status, + new: status, + }); + const newRegistration = await DroneRegistration.findOneAndUpdate( + { _id: registration._id }, + { $set: { status } }, + { new: true, populate: this.populateDroneRegistration }, + ); + if (!newRegistration) { + const error = new Error("drone registration has been removed"); + error.statusCode = 404; + throw error; + } + return newRegistration; + } +} + +export default new DroneService(); diff --git a/gadget-code/src/services/notification.ts b/gadget-code/src/services/notification.ts new file mode 100644 index 0000000..4af0539 --- /dev/null +++ b/gadget-code/src/services/notification.ts @@ -0,0 +1,19 @@ +// services/notification.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { DtpService } from "../lib/service.js"; + +class NotificationService extends DtpService { + get name(): string { + return "NotificationService"; + } + get slug(): string { + return "notification"; + } + + async start(): Promise {} + async stop(): Promise {} +} + +export default new NotificationService(); diff --git a/gadget-code/src/services/session.ts b/gadget-code/src/services/session.ts new file mode 100644 index 0000000..d9bcad3 --- /dev/null +++ b/gadget-code/src/services/session.ts @@ -0,0 +1,144 @@ +// src/services/session.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../config/env.js"; + +import { Types } from "mongoose"; +import { Request } from "express"; + +import jwt from "jsonwebtoken"; +import dayjs from "dayjs"; +import geoip from "geoip-lite"; + +import { IUser } from "../models/user.js"; +import { IWebToken, WebToken } from "../models/web-token.js"; +import { WebVisit } from "../models/web-visit.js"; + +import UserService from "../services/user.js"; +import { DtpService } from "../lib/service.js"; + +export enum SessionType { + WEB = "web", + JWT = "jwt", +} + +interface UserWebToken { + _id: string; + email: string; + displayName: string; + webToken: IWebToken | Types.ObjectId; +} + +class SessionService extends DtpService { + get name(): string { + return "SessionService"; + } + get slug(): string { + return "session"; + } + + async start(): Promise {} + async stop(): Promise {} + + async createJsonWebToken(user: IUser): Promise { + const NOW = new Date(); + const webToken = new WebToken(); + webToken.created = NOW; + webToken.expires = dayjs(NOW).add(1, "hour").toDate(); + webToken.user = user._id; + + const payload: UserWebToken = { + _id: user._id.toString(), + email: user.email, + displayName: user.displayName, + webToken: webToken._id, + }; + const token = jwt.sign(payload, env.auth.jwtSecret, { + expiresIn: "1h", + }); + + webToken.token = token; + await webToken.save(); + + return token; + } + + async verifyJsonWebToken(token: string): Promise { + try { + const NOW = new Date(); + const payload = jwt.verify(token, env.auth.jwtSecret) as UserWebToken; + const userId = Types.ObjectId.createFromHexString(payload._id); + + const webToken = await WebToken.findOne({ _id: payload.webToken }); + if (!webToken) { + const error = new Error("Invalid JSON Web Token"); + error.name = "InvalidToken"; + error.statusCode = 401; + throw error; + } + if (webToken.expires < NOW) { + await WebToken.deleteOne({ _id: webToken._id }); + const error = new Error("JSON Web Token has expired"); + error.name = "ExpiredToken"; + error.statusCode = 401; + throw error; + } + if (!userId.equals(webToken.user._id)) { + const error = new Error("JSON Web Token ownership mismatch"); + error.name = "TokenOwnershipMismatch"; + error.statusCode = 401; + throw error; + } + + const user = await UserService.getById(userId); + return user; + } catch (cause) { + this.log.error("failed to verify JSON Web Token", { + token, + error: cause, + }); + const error = new Error("Invalid JSON Web Token", { cause }); + error.name = "TokenVerifyError"; + error.statusCode = 401; + throw error; + } + } + + async revokeJsonWebToken(token: string): Promise { + const payload = jwt.verify(token, env.auth.jwtSecret) as UserWebToken; + this.log.info("revoking JSON Web Token", { tokenId: payload.webToken }); + await WebToken.deleteOne({ _id: payload.webToken }); + } + + async recordWebVisit(req: Request): Promise { + const NOW = new Date(); + const visit = new WebVisit(); + visit.created = NOW; + visit.url = req.url; + if (req.user) { + visit.user = req.user._id; + } + visit.userAgent = req.headers["user-agent"]; + visit.referrer = req.headers["referer"]; + visit.ipAddress = req.ip; + + if (req.ip) { + const ipLookup = geoip.lookup(req.ip); + if (ipLookup) { + visit.country = ipLookup.country; + visit.region = ipLookup.region; + visit.eu = ipLookup.eu === "1"; + visit.timezone = ipLookup.timezone; + visit.city = ipLookup.city; + visit.latitude = ipLookup.ll[0]; + visit.longitude = ipLookup.ll[1]; + visit.metroCode = ipLookup.metro; + } + } + + await visit.save(); + } +} + +export default new SessionService(); diff --git a/gadget-code/src/services/storage.ts b/gadget-code/src/services/storage.ts new file mode 100644 index 0000000..83a0274 --- /dev/null +++ b/gadget-code/src/services/storage.ts @@ -0,0 +1,201 @@ +// src/services/storage.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "../config/env.js"; + +import { Stream as NodeStream } from "node:stream"; + +import { Client as MinioClient, BucketItemStat } from "minio"; + +/* + * WebApp needs the type, and it's not exposed any other way. It's not even an + * uncommon type. If there's a better way to expose the type, I don't know it. + */ +import { UploadedObjectInfo } from "../../node_modules/minio/dist/esm/internal/type.mjs"; + +export type MinioObjectMetadata = Record; + +export interface MinioFile { + bucket: string; + key: string; + filePath?: string; + metadata?: MinioObjectMetadata; +} + +export interface MinioStreamFileRange { + offset: number; + length: number; +} + +export interface MinioStreamFile extends MinioFile { + range?: MinioStreamFileRange; +} + +export interface MinioBuffer { + bucket: string; + key: string; + buffer: Buffer; + metadata?: MinioObjectMetadata; +} + +import { DtpService } from "../lib/service.js"; + +class StorageService extends DtpService { + minio?: MinioClient; + + get name(): string { + return "StorageService"; + } + get slug(): string { + return "storage"; + } + + constructor() { + super(); + } + + async start(): Promise { + try { + this.log.info("creating MinIO transport", { + endPoint: env.minio.endpoint, + port: env.minio.port, + useSSL: env.minio.useSsl, + accessKey: env.minio.accessKey, + }); + this.minio = new MinioClient({ + endPoint: env.minio.endpoint, + port: env.minio.port, + useSSL: env.minio.useSsl, + accessKey: env.minio.accessKey, + secretKey: env.minio.secretKey, + }); + } catch (error) { + this.log.error("failed to connect to S3/MinIO", { error }); + const e = new Error("Not connected to storage", { cause: error }); + e.statusCode = 500; + throw e; + } + } + + async stop(): Promise {} + + async uploadBuffer(buffer: MinioBuffer): Promise { + if (!this.minio) { + const error = new Error("Not connected to storage"); + error.statusCode = 503; + throw error; + } + const response = await this.minio.putObject( + buffer.bucket, + buffer.key, + buffer.buffer, + buffer.buffer.length, + buffer.metadata, + ); + this.log.debug("uploaded buffer to storage", { response }); + } + + async uploadFile(fileInfo: MinioFile): Promise { + if (!this.minio) { + const error = new Error("Not connected to storage"); + error.statusCode = 503; + throw error; + } + if (!fileInfo.filePath) { + const error = new Error("Must specify filePath of file to upload"); + error.statusCode = 400; + throw error; + } + return this.minio.fPutObject( + fileInfo.bucket, + fileInfo.key, + fileInfo.filePath, + fileInfo.metadata, + ); + } + + async statFile(file: MinioFile): Promise { + return this.statObject(file.bucket, file.key); + } + + async statObject(bucket: string, key: string): Promise { + if (!this.minio) { + const error = new Error("Not connected to storage"); + error.statusCode = 503; + throw error; + } + this.log.debug("retrieving object status", { bucket, key }); + return this.minio.statObject(bucket, key); + } + + async downloadFile(fileInfo: MinioFile): Promise { + if (!this.minio) { + const error = new Error("Not connected to storage"); + error.statusCode = 503; + throw error; + } + if (!fileInfo.filePath) { + const error = new Error("Must specify filePath of file to download"); + error.statusCode = 400; + throw error; + } + this.log.debug("downloading object to file", { + bucket: fileInfo.bucket, + key: fileInfo.key, + filePath: fileInfo.filePath, + }); + return this.minio.fGetObject( + fileInfo.bucket, + fileInfo.key, + fileInfo.filePath, + ); + } + + async openDownloadStream(fileInfo: MinioStreamFile): Promise { + if (!this.minio) { + const error = new Error("Not connected to storage"); + error.statusCode = 503; + throw error; + } + + if (fileInfo.range) { + this.log.debug("fetching partial object", { + bucket: fileInfo.bucket, + key: fileInfo.key, + range: fileInfo.range, + }); + const stream: NodeStream = await this.minio.getPartialObject( + fileInfo.bucket, + fileInfo.key, + fileInfo.range.offset, + fileInfo.range.length, + ); + return stream; + } + + this.log.debug("fetching object", { + bucket: fileInfo.bucket, + key: fileInfo.key, + }); + const stream: NodeStream = await this.minio.getObject( + fileInfo.bucket, + fileInfo.key, + ); + return stream; + } + + async removeObject(bucket: string, key: string): Promise { + if (!this.minio) { + const error = new Error("Not connected to storage"); + error.statusCode = 503; + throw error; + } + this.log.debug("removing object", { bucket, key }); + await this.minio.removeObject(bucket, key); + } +} + +export { NodeStream }; + +export default new StorageService(); diff --git a/gadget-code/src/services/user.ts b/gadget-code/src/services/user.ts new file mode 100644 index 0000000..8d31e8e --- /dev/null +++ b/gadget-code/src/services/user.ts @@ -0,0 +1,312 @@ +// src/services/user.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import assert from "node:assert"; + +import { MongooseBaseQueryOptions, Types } from "mongoose"; +import { v4 as uuidv4 } from "uuid"; +import { filterText } from "dtp-cleantext"; + +import User, { IUser } from "../models/user.ts"; +import ContactService from "./contact.ts"; +import CryptoService from "./crypto.ts"; + +import { DtpPaginationParameters } from "../lib/pagination-parameters.ts"; + +import { DtpService } from "../lib/service.js"; + +export interface IUserUpdate { + email: string; + displayName?: string; + password?: string; +} + +export interface IUserAdminUpdate { + email: string; + displayName?: string; + password?: string; + "flags.isEmailVerified": string; + "flags.isAdmin": string; + "flags.isTest": string; + "flags.isBanned": string; +} +export interface IUserLibrary { + users: IUser[]; + totalUserCount: number; +} + +class UserService extends DtpService { + get name(): string { + return "UserService"; + } + get slug(): string { + return "user"; + } + + async start(): Promise {} + async stop(): Promise {} + + async createDeferred( + email: string, + password: string, + displayName?: string, + ): Promise { + if (!email) { + const error = new Error("must specify email address"); + error.statusCode = 400; + throw error; + } + if (!password) { + const error = new Error("must specify password"); + error.statusCode = 400; + throw error; + } + + if (displayName) { + displayName = filterText(displayName); + if (displayName.length < 3 || displayName.length > 30) { + const err = new Error( + "Display Name must be between 3-30 characters in length.", + ); + err.statusCode = 400; + throw err; + } + } + + /* + * If we have an existing user, abort. + */ + + if (await this.getByEmail(email)) { + const err = new Error("That email address is already registered."); + err.statusCode = 400; + throw err; + } + + /* + * Create a new User + */ + + const user = new User(); + user.email = filterText(email); + user.email_lc = user.email.toLowerCase(); + + user.passwordSalt = uuidv4(); + user.password = await CryptoService.maskPassword( + user.passwordSalt, + password.trim(), + ); + + if (displayName) { + user.displayName = displayName; + } + + user.flags = { + isEmailVerified: false, + isAdmin: false, + isTest: false, + isBanned: false, + }; + + return user; + } + + async create( + email: string, + password: string, + displayName?: string, + ): Promise { + const user = await this.createDeferred(email, password, displayName); + await user.save(); + + await ContactService.sendVerificationEmail(user); + + return user.toObject(); + } + + async updateForUser(user: IUser, def: IUserUpdate): Promise { + const update: MongooseBaseQueryOptions = { $set: {} }; + let needsEmailVerification = false; + + if (def.email && def.email !== user.email) { + update.$set.email = filterText(def.email); + update.$set.email_lc = update.$set.email.toLowerCase(); + update.$set["flags.isEmailVerified"] = false; + needsEmailVerification = true; + this.log.info("updating user email address", { + user: { _id: user._id }, + email: update.$set.email, + }); + } + + if (def.password) { + this.log.info("updating user password", { user: { _id: user._id } }); + update.$set.passwordSalt = uuidv4(); + update.$set.password = await CryptoService.maskPassword( + update.$set.passwordSalt, + def.password.trim(), + ); + } + + const newUser = await User.findOneAndUpdate({ _id: user._id }, update, { + new: true, + }); + if (!newUser) { + const error = new Error(`User ${user._id} not found`); + error.statusCode = 400; + throw error; + } + + if (needsEmailVerification) { + await ContactService.sendVerificationEmail(newUser); + } + + return newUser; + } + + async updateForAdmin(user: IUser, def: IUserAdminUpdate): Promise { + const update: MongooseBaseQueryOptions = { $set: {} }; + + if (def.email && def.email !== user.email) { + update.$set.email = filterText(def.email); + update.$set.email_lc = update.$set.email.toLowerCase(); + this.log.info("updating user email address", { + user: { _id: user._id }, + email: update.$set.email, + }); + } + + if (def.password) { + this.log.info("updating user password", { user: { _id: user._id } }); + update.$set.passwordSalt = uuidv4(); + update.$set.password = await CryptoService.maskPassword( + update.$set.passwordSalt, + def.password.trim(), + ); + } + + update.$set["flags.isEmailVerified"] = + def["flags.isEmailVerified"] === "on"; + update.$set["flags.isAdmin"] = def["flags.isAdmin"] === "on"; + update.$set["flags.isTest"] = def["flags.isTest"] === "on"; + update.$set["flags.isBanned"] = def["flags.isBanned"] === "on"; + + this.log.info("updating user flags", { + user: { _id: user._id }, + flags: { + isEmailVerified: update.$set["flags.isEmailVerified"], + isAdmin: update.$set["flags.isAdmin"], + isTest: update.$set["flags.isTest"], + isBanned: update.$set["flags.isBanned"], + }, + }); + + await User.updateOne({ _id: user._id }, update); + } + + async setEmailVerified(user: IUser, isVerified: boolean): Promise { + this.log.info("setting user email verified status", { + user: { _id: user._id }, + isVerified, + }); + + const newUser = await User.findOneAndUpdate( + { _id: user._id }, + { $set: { "flags.isEmailVerified": isVerified } }, + { new: true }, + ); + if (!newUser) { + const error = new Error(`User ${user._id} not found`); + error.statusCode = 400; + throw error; + } + + return newUser; + } + + async getRecent( + pagination: DtpPaginationParameters, + query?: string, + ): Promise { + const search: MongooseBaseQueryOptions = {}; + if (query) { + this.log.debug("searching User", { query }); + search.$text = { $search: query }; + } + const users = await User.find(search) + .sort({ created: -1 }) + .skip(pagination.skip) + .limit(pagination.cpp); + const totalUserCount = await User.countDocuments(search); + return { users, totalUserCount }; + } + + async getByEmail(email: string): Promise { + if (!email) { + throw new Error("must specify email address"); + } + const user = await User.findOne({ email_lc: email.trim().toLowerCase() }); + if (!user) { + return; + } + return user; + } + + async getById(userId: Types.ObjectId): Promise { + if (!userId) { + const error = new Error("must specify email address"); + error.statusCode = 400; + throw error; + } + + const user = await User.findOne({ _id: userId }); + if (!user) { + const err = new Error(`User ${userId} not found`); + err.statusCode = 400; + throw err; + } + + return user; + } + + async authenticate(email: string, password: string): Promise { + if (!email) { + const error = new Error("Must provide your email address"); + error.statusCode = 400; + throw error; + } + if (!password) { + const error = new Error("Must provide your password"); + error.statusCode = 400; + throw error; + } + + const emailLc = email.trim().toLowerCase(); + const user = await User.findOne({ email_lc: emailLc }).select( + "+passwordSalt +password", + ); + if (!user) { + const error = new Error("Invalid email address or password"); + error.statusCode = 400; + throw error; + } + assert(user.password && user.passwordSalt, "user authentication failed"); + + const maskedPassword = await CryptoService.maskPassword( + user.passwordSalt, + password.trim(), + ); + if (maskedPassword !== user.password) { + const error = new Error("Invalid email address or password"); + error.statusCode = 400; + throw error; + } + + delete user.passwordSalt; + delete user.password; + return user; + } +} + +export default new UserService(); diff --git a/gadget-code/src/web-app.ts b/gadget-code/src/web-app.ts new file mode 100644 index 0000000..677b75c --- /dev/null +++ b/gadget-code/src/web-app.ts @@ -0,0 +1,551 @@ +// web-app.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import env from "./config/env.js"; +import assert from "node:assert"; + +import path from "node:path"; +import http from "node:http"; + +import { + DisconnectReason, + ExtendedError, + Socket, + Server as SocketIOServer, +} from "socket.io"; + +import "./lib/db.js"; +import redis from "./lib/redis.js"; + +import cookieParser from "cookie-parser"; +import compress from "compression"; +import methodOverride from "method-override"; +import favicon from "serve-favicon"; + +import session, { SessionOptions } from "express-session"; +import { RedisStore } from "connect-redis"; + +import express, { NextFunction, Request, Response } from "express"; +import morgan from "morgan"; +import * as rfs from "rotating-file-stream"; + +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime.js"; +dayjs.extend(relativeTime); + +import numeral from "numeral"; + +type SameSiteOption = boolean | "lax" | "strict" | "none" | undefined; + +import { DtpLog } from "./lib/log.js"; +import { DtpComponent } from "./lib/component.js"; + +import { ApiController } from "./controllers/api.js"; +import { AuthController } from "./controllers/auth.js"; +import { HomeController } from "./controllers/home.js"; +import { UserController } from "./controllers/user.js"; + +import ApiClient from "./services/api-client.js"; +import ContactService from "./services/contact.js"; +import DroneService from "./services/drone.js"; +import SessionService, { SessionType } from "./services/session.js"; +import StorageService from "./services/storage.js"; + +import { Types } from "mongoose"; +import { User } from "./models/user.js"; + +import { SocketSessionType } from "./lib/socket-session.js"; +import { CodeSession } from "./lib/code-session.js"; +import { DroneSession } from "./lib/drone-session.js"; + +class DtpWebAppServer implements DtpComponent { + private log: DtpLog; + + private app?: express.Application; + private server?: http.Server; + public io?: SocketIOServer; + + private codeSessions: Map = new Map< + string, + CodeSession + >(); + private droneSessions: Map = new Map< + string, + DroneSession + >(); + + get name(): string { + return "DtpWebAppServer"; + } + get slug(): string { + return "web-app"; + } + + constructor() { + this.log = new DtpLog(this); + } + + async start(): Promise { + this.hookProcessSignals(); + + await this.startServices(); + await this.createExpressApp(); + await this.startHttpServer(); + + this.log.info( + `DTP application server online at http://${env.https.address}:${env.https.port}/`, + ); + } + + async stop(): Promise { + await this.stopHttpServer(); + return 0; + } + + async startServices(): Promise { + await ApiClient.start(); + await ContactService.start(); + await StorageService.start(); + } + + async createExpressApp(): Promise { + this.log.info("creating ExpressJS app"); + this.app = express(); + this.app.set("trust proxy", true); + this.app.set("x-powered-by", false); + + this.app.locals.env = env; + this.app.locals.pkg = env.pkg; + this.app.locals.site = env.site; + this.app.locals.dayjs = dayjs; + this.app.locals.numeral = numeral; + + if (env.log.https.enabled) { + this.log.info("creating HTTPS access log", { + name: env.log.https.name, + path: env.log.https.path, + }); + const httpLogStream = rfs.createStream( + env.log.https.name || "gadget-code-access.log", + { + interval: "1d", + path: env.log.https.path, + compress: "gzip", + }, + ); + this.app.use(morgan(env.log.https.format, { stream: httpLogStream })); + } + + /* + * Static file services + */ + this.app.use( + favicon(path.join(env.root, "assets", "icon", "icon-32x32.png")), + ); + this.app.use("/assets", express.static(path.resolve(env.root, "assets"))); + this.app.use( + "/client", + express.static(path.resolve(env.root, "dist", "client")), + ); + + /* + * REST, User, and Session support + */ + + this.app.use(async (req: Request, res: Response, next: NextFunction) => { + await SessionService.recordWebVisit(req); + + if (req.query) { + res.locals.query = req.query; + } + + const contentType = req.get("content-type") || ""; + if (!contentType.includes("application/json")) { + return next(); + } + + let data: string = ""; + req.on("data", (chunk) => { + data += chunk.toString(); + }); + req.on("end", () => { + req.rawBody = data; + try { + const parsedData = JSON.parse(data); + req.body = parsedData; + } catch (err) { + return next(err); + } + next(); + }); + }); + + this.app.use(express.json()); + this.app.use(express.urlencoded({ extended: true })); + this.app.use(cookieParser(env.session.secret)); + this.app.use(compress()); + this.app.use(methodOverride()); + + const ONE_WEEK = 1000 * 60 * 60 * 24 * 7; + const SESSION_DURATION = ONE_WEEK; + + /* + * ExpressJS session configuration + */ + + const store = new RedisStore({ + client: redis, + }); + + assert(env.session.secret, "Must define an HTTP session secret"); + const sessionConfig: SessionOptions = { + name: `mtsess-${env.site.domainKey}-${env.NODE_ENV}`, + secret: env.session.secret, + resave: true, + saveUninitialized: true, + proxy: env.session.trustProxy, + cookie: { + path: "/", + httpOnly: true, + secure: env.session.cookie.secure, + sameSite: env.session.cookie.sameSite as SameSiteOption, + maxAge: SESSION_DURATION, + }, + store, + }; + this.app.use(session(sessionConfig)); + this.app.use(this.restoreUserSession.bind(this)); + + this.app.use("/", await this.createAppRouter()); + + this.app.use(this.defaultErrorHandler.bind(this)); + } + + async createAppRouter(): Promise { + const router: express.Router = express.Router(); + + const authController = new AuthController(); + await authController.start(); + this.log.info("mounting AuthController", { route: authController.route }); + router.use(authController.route, authController.router); + + const apiController = new ApiController(); + await apiController.start(); + this.log.info("mounting ApiController", { + route: apiController.route, + }); + router.use(apiController.route, apiController.router); + + const userController = new UserController(); + await userController.start(); + this.log.info("mounting UserController", { + route: userController.route, + }); + router.use(userController.route, userController.router); + + const homeController = new HomeController(); + await homeController.start(); + this.log.info("mounting HomeController", { route: homeController.route }); + router.use(homeController.route, homeController.router); + + return router; + } + + async startHttpServer(): Promise { + return new Promise((resolve) => { + assert(this.app, "ExpressJS app instance is required"); + this.log.info("starting HTTP server", { + address: env.https.address, + port: env.https.port, + }); + + /* + * Create HTTP server + */ + this.server = http.createServer(this.app); + + /* + * Create Socket.io server + */ + this.io = new SocketIOServer(this.server, { + cors: { + origin: "*", + methods: ["GET", "POST"], + }, + }); + this.io.use(this.onSocketAuth.bind(this)); + this.io.on("connection", this.onSocketConnection.bind(this)); + this.log.info("socket.io server initialized"); + + /* + * Start HTTP server + */ + this.server.listen( + env.https.port, + env.https.address, + env.https.backlog, + () => { + this.log.debug("HTTP server started"); + resolve(); + }, + ); + }); + } + + async onSocketAuth(socket: Socket, next: (err?: ExtendedError) => void) { + const token = socket.handshake.auth.token; + this.log.debug("received socket authentication request", { token }); + if (!token) { + this.log.warn("socket connection rejected: no token provided"); + return next(new Error("Authentication required")); + } + + /* + * Try first to validate as a User JWT session + */ + try { + const user = await SessionService.verifyJsonWebToken(token); + this.log.info("socket authenticated as User"); + + const session: CodeSession = new CodeSession(socket, user); + this.codeSessions.set(socket.id, session); + + socket.data = { sessionType: SocketSessionType.Code }; + socket.on("disconnect", (reason: DisconnectReason, extra?: unknown) => { + this.onSocketDisconnect(socket, reason, extra); + }); + + return next(); + } catch (cause) { + const error = cause as Error; + if (error.name !== "TokenVerifyError") { + this.log.warn("socket connection rejected: invalid token", { + error, + }); + return next(new Error("Invalid authentication token")); + } + // fall through to next test + } + + /* + * If not a User JWT, try to validate as a Drone session + */ + try { + const registrationId = Types.ObjectId.createFromHexString(token); + const registration = await DroneService.getById(registrationId); + this.log.info("socket authenticated as Drone"); + + const droneSession: DroneSession = new DroneSession(socket, registration); + this.droneSessions.set(socket.id, droneSession); + + socket.data = { sessionType: SocketSessionType.Drone }; + socket.on("disconnect", (reason: DisconnectReason, extra?: unknown) => { + this.onSocketDisconnect(socket, reason, extra); + }); + + return next(); + } catch (error) { + this.log.warn("socket connection rejected: invalid auth token", { + error, + }); + next(new Error("Invalid authentication token")); + } + } + + onSocketConnection(socket: Socket) { + switch (socket.data.sessionType) { + case SocketSessionType.Code: + return this.onSocketConnectCode(socket); + case SocketSessionType.Drone: + return this.onSocketConnectDrone(socket); + default: + break; + } + this.log.error("invalid socket session type during connect"); + } + + onSocketDisconnect( + socket: Socket, + reason: DisconnectReason, + extra?: unknown, + ) { + this.log.info("socket disconnect", { reason, extra }); + switch (socket.data.sessionType) { + case SocketSessionType.Code: + this.log.info("closing code socket session", { id: socket.id }); + this.codeSessions.delete(socket.id); + return; + + case SocketSessionType.Drone: + this.log.info("closing drone socket session", { id: socket.id }); + this.droneSessions.delete(socket.id); + return; + + default: + break; + } + + this.log.error("invalid session type in socket disconnect", { + id: socket.id, + data: socket.data, + }); + } + + onSocketConnectCode(socket: Socket) { + const session = this.codeSessions.get(socket.id); + if (!session) { + this.log.warn("invalid code session during connect"); + socket.disconnect(true); + return; + } + this.log.info("code socket connected", { + id: socket.id, + userId: session.user._id.toHexString(), + email: session.user.email, + }); + } + + onSocketConnectDrone(socket: Socket) { + const session = this.droneSessions.get(socket.id); + if (!session) { + this.log.warn("invalid drone session during connect"); + socket.disconnect(true); + return; + } + this.log.info("drone socket connected", { + id: socket.id, + userId: session.user._id.toHexString(), + email: session.user.email, + registration: session.registration, + }); + } + + async stopHttpServer(): Promise { + return new Promise((resolve, reject) => { + if (!this.server) { + return; + } + + this.log.info("closing all HTTP connections"); + this.server.closeAllConnections(); + + if (!this.server.listening) { + delete this.server; + return resolve(); + } + + this.log.info("closing HTTP server"); + this.server.close((error) => { + if (error) { + return reject(error); + } + delete this.server; + resolve(); + }); + }); + } + + async defaultErrorHandler( + err: Error, + req: Request, + res: Response, + _next: NextFunction, + ) { + this.log.error("ExpressJS error", { error: err }); + res.locals.errorCode = err.statusCode || 500; + res.locals.error = err; + if (req.session && req.session.type == SessionType.JWT) { + res.status(res.locals.errorCode).json({ + success: false, + message: err.message, + }); + return; + } + + res.status(res.locals.errorCode).json({ + success: false, + message: err.message, + }); + } + + async restoreUserSession( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + let token; + if (req.headers["authorization"]) { + const tokens = req.headers["authorization"].split(" "); + token = tokens[tokens.length - 1]; + } + if (!token && (!req.session || !req.session.user)) { + return next(); + } + + if (token) { + this.log.debug("restoring session from JWT", { token }); + req.user = await SessionService.verifyJsonWebToken(token); + } else { + const userId = Types.ObjectId.createFromHexString(req.session.user._id); + this.log.debug("restoring session from HTTP session", { + sessionId: req.session.id, + }); + req.user = await User.findOne({ _id: userId }); + } + + res.locals.user = req.user; + if (!res.locals.user) { + req.session.destroy((err: Error) => { + if (err) { + return next(err); + } + return next(); + }); + return; + } + + return next(); + } + + hookProcessSignals(): void { + process.title = this.name; + process.on("unhandledRejection", async (error: Error, p) => { + this.log.error("Unhandled rejection", { + error: error, + promise: p, + stack: error.stack, + }); + + const exitCode: number = await this.stop(); + process.nextTick(() => { + process.exit(exitCode); + }); + }); + + process.on("warning", (error) => { + if (error.name === "DeprecationWarning") { + return; + } + this.log.alert("warning", { error }); + }); + + process.once("SIGINT", async () => { + this.log.info("SIGINT received (requesting shutdown)"); + const exitCode = await this.stop(); + process.nextTick(() => { + process.exit(exitCode); + }); + }); + } +} + +(async () => { + try { + const server = new DtpWebAppServer(); + await server.start(); + } catch (error) { + console.error("DTP Web App server error", { error }); + process.exit(-1); + } +})(); diff --git a/gadget-code/src/web-cli.ts b/gadget-code/src/web-cli.ts new file mode 100644 index 0000000..9d47a68 --- /dev/null +++ b/gadget-code/src/web-cli.ts @@ -0,0 +1,341 @@ +// src/web-cli.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { v4 as uuidv4 } from "uuid"; +import { Types } from "mongoose"; + +import "./lib/db.js"; + +import ApiClient, { ApiClientStatus } from "./models/api-client.js"; +import User from "./models/user.js"; + +import ApiClientService from "./services/api-client.js"; +import CryptoService from "./services/crypto.js"; +import UserService from "./services/user.js"; + +import { DtpProcess } from "./lib/process.js"; + +class DtpWebCli extends DtpProcess { + get name(): string { + return "DtpWebCli"; + } + get slug(): string { + return "dtp-web-cli"; + } + + constructor() { + super(); + } + + async run(argv: string[]): Promise { + const cmd = argv.shift(); + if (!cmd) { + throw new Error("must specify command"); + } + switch (cmd) { + case "admin": + return this.onAdminCmd(argv); + + case "api-client": + return this.onApiClientCmd(argv); + + case "user": + return this.onUserCmd(argv); + + default: + break; + } + throw new Error(`unknown command: ${cmd}`); + } + + async onAdminCmd(argv: string[]): Promise { + const action = argv.shift(); + if (!action) { + throw new Error("must specify user command action"); + } + switch (action) { + case "grant": + return this.onAdminGrant(argv); + case "revoke": + return this.onAdminRevoke(argv); + default: + break; + } + throw new Error(`unknown user command action: ${action}`); + } + + async onAdminGrant(argv: string[]): Promise { + const email = argv.shift(); + if (!email) { + throw new Error("must specify email for admin grant"); + } + const user = await User.findOne({ + email_lc: email.trim().toLowerCase(), + }); + if (!user) { + throw new Error(`user ${email} not found`); + } + + user.flags.isAdmin = true; + await user.save(); + + this.log.info(`admin rights granted to ${email}`); + } + + async onAdminRevoke(argv: string[]): Promise { + const email = argv.shift(); + if (!email) { + throw new Error("must specify email for admin revoke"); + } + + const user = await User.findOne({ + email_lc: email.trim().toLowerCase(), + }); + if (!user) { + throw new Error(`user ${email} not found`); + } + + user.flags.isAdmin = false; + await user.save(); + + this.log.info(`admin rights revoked from ${email}`); + } + + async onApiClientCmd(argv: string[]): Promise { + const action = argv.shift(); + if (!action) { + throw new Error("must specify api client command action"); + } + switch (action) { + case "add": + return this.onApiClientAdd(argv); + + case "ls": + return this.onApiClientList(argv); + + case "status": + return this.onApiClientSetStatus(argv); + + case "remove": + return this.onApiClientRemove(argv); + + default: + break; + } + throw new Error(`unknown api client command action: ${action}`); + } + + async onApiClientAdd(argv: string[]): Promise { + const name = argv.shift(); + const description = argv.shift(); + + const client = await ApiClientService.create({ + name, + description, + }); + this.log.info("api client added", { + client: { + _id: client._id, + secret: client.secret, + name: client.name, + }, + }); + } + + async onApiClientList(_argv: string[]): Promise { + const clients = await ApiClient.find({ status: ApiClientStatus.Active }) + .sort({ name: 1 }) + .lean(); + console.log("Name".padEnd(20), "Client ID".padEnd(24), "Secret"); + console.log( + "--------------------------------------------------------------------------------" + ); + for (const client of clients) { + console.log(client.name.padEnd(20), client._id.toString(), client.secret); + } + } + + async onApiClientSetStatus(argv: string[]): Promise { + const clientId = argv.shift(); + if (!clientId) { + throw new Error("client ID is required"); + } + const clientIdObj = Types.ObjectId.createFromHexString(clientId); + const client = await ApiClientService.getById(clientIdObj); + if (!client) { + throw new Error("Client not found"); + } + + const status = argv.shift() as ApiClientStatus; + if (!status) { + throw new Error("New client status is required"); + } + + await ApiClientService.setStatus(client, status); + this.log.info("API client status updated", { _id: clientId, status }); + } + + async onApiClientRemove(argv: string[]): Promise { + const clientId = argv.shift(); + if (!clientId) { + throw new Error("client ID is required"); + } + const clientIdObj = Types.ObjectId.createFromHexString(clientId); + const client = await ApiClientService.getById(clientIdObj); + if (!client) { + throw new Error("Client not found"); + } + await ApiClientService.remove(client); + this.log.info("API client removed", { _id: clientId }); + } + + async onUserCmd(argv: string[]): Promise { + const action = argv.shift(); + if (!action) { + throw new Error("must specify user command action"); + } + switch (action) { + case "add": + return this.onUserAdd(argv); + case "password": + return this.onUserPassword(argv); + case "remove": + return this.onUserRemove(argv); + case "ban": + return this.onUserBan(argv); + default: + break; + } + throw new Error(`unknown user command action: ${action}`); + } + + async onUserAdd(argv: string[]): Promise { + const email = argv.shift(); + if (!email) { + throw new Error("must specify email address"); + } + + const password = argv.shift(); + if (!password) { + throw new Error("must specify password"); + } + + const displayName: string | undefined = argv.shift(); + + const user = await UserService.create(email, password, displayName); + + this.log.info( + `user created: id:${user._id.toHexString()}, email:${user.email}` + ); + } + + async onUserRemove(argv: string[]): Promise { + let email = argv.shift(); + if (!email) { + throw new Error("must specify email address"); + } + + email = email.trim().toLowerCase(); + const user = await User.findOne({ email }).lean(); + if (!user) { + throw new Error(`user not found: ${email}`); + } + + await User.deleteOne({ _id: user._id }); + this.log.info(`user ${email} removed`); + } + + async onUserBan(argv: string[]): Promise { + const email = argv.shift(); + if (!email) { + throw new Error("must specify email address"); + } + + const email_lc = email.trim().toLowerCase(); + const user = await User.findOne({ email_lc }).lean(); + if (!user) { + throw new Error(`user not found: ${email}`); + } + + await User.updateOne( + { _id: user._id }, + { + $set: { + "flags.isBanned": true, + }, + } + ); + this.log.info(`user ${email} banned`); + } + + async onUserUnban(argv: string[]): Promise { + const email = argv.shift(); + if (!email) { + throw new Error("must specify email address"); + } + + const email_lc = email.trim().toLowerCase(); + const user = await User.findOne({ email_lc }).lean(); + if (!user) { + throw new Error(`user not found: ${email}`); + } + + await User.updateOne( + { _id: user._id }, + { + $set: { + "flags.isBanned": false, + }, + } + ); + this.log.info(`user ${email} unbanned`); + } + + async onUserPassword(argv: string[]): Promise { + let email = argv.shift(); + if (!email) { + throw new Error("must specify email address"); + } + + const password = argv.shift(); + if (!password) { + throw new Error("must specify password"); + } + + const email_lc = email.trim().toLowerCase(); + const user = await User.findOne({ email_lc }); + if (!user) { + throw new Error(`user not found: ${email}`); + } + + user.passwordSalt = uuidv4(); + user.password = await CryptoService.maskPassword( + user.passwordSalt, + password + ); + await user.save(); + + this.log.info(`user ${email} password changed`); + } + + async start(): Promise { + await this.startServices(); + } + + async startServices(): Promise { + await ApiClientService.start(); + } +} + +(async () => { + try { + const cli = new DtpWebCli(); + await cli.start(); + await cli.run(process.argv.slice(2)); + process.exit(0); + } catch (error) { + console.error((error as Error).message); + process.exit(-1); + } +})(); diff --git a/gadget-code/ssl/install-certs b/gadget-code/ssl/install-certs new file mode 100755 index 0000000..9e02dca --- /dev/null +++ b/gadget-code/ssl/install-certs @@ -0,0 +1,37 @@ +#!/bin/bash +set -e # stop and exit on any error + +PROJECT_DOMAIN="code-dev.g4dge7.com" +PROJECT_NAME="Gadget Code" + +# +# Add certificates to OS store for general system use +# + +echo "Installing Root CA..." +sudo cp "${PROJECT_DOMAIN}.rootCA.crt" /usr/local/share/ca-certificates/ + +echo "Installing self-signed development certificate..." +sudo cp "${PROJECT_DOMAIN}.crt" /usr/local/share/ca-certificates/ + +sudo update-ca-certificates --fresh + +# +# Add development certificates to NSS database for Chromium and others +# + +certutil -A -d ~/.pki/nssdb/ \ + -t "T,T,T" \ + -i "${PROJECT_DOMAIN}.rootCA.crt" \ + -n "${PROJECT_NAME} Root CA" + +certutil -A -d ~/.pki/nssdb/ \ + -t "C,C,C" \ + -i "${PROJECT_DOMAIN}.crt" \ + -n "${PROJECT_NAME} Development" + +# +# Display list of registered certificates in NSS database +# + +certutil -L -d ~/.pki/nssdb/ diff --git a/gadget-code/ssl/mkcert b/gadget-code/ssl/mkcert new file mode 100755 index 0000000..f1671b9 --- /dev/null +++ b/gadget-code/ssl/mkcert @@ -0,0 +1,97 @@ +#!/bin/bash +set -e # stop and exit on any error + +PROJECT_DOMAIN="code-dev.g4dge7.com" +PROJECT_NAME="Gadget Code" + +WITH_INSTALL=1 + +for arg in "$@"; do + if [ "$arg" == "--without-install" ]; then + WITH_INSTALL=0 + fi +done + +# Clean up old files +rm -f *crt *key + +# +# ROOT CA +# + +# Generate Root CA private key +echo "Generating Root CA..." +openssl genrsa -des3 -out ${PROJECT_DOMAIN}.rootCA.key 2048 + +# Create Root CA self-signed certificate +openssl req -x509 \ + -new -nodes -key ${PROJECT_DOMAIN}.rootCA.key \ + -sha256 -days 1024 \ + -out ${PROJECT_DOMAIN}.rootCA.crt \ + -subj "/C=US/ST=Pennsylvania/L=Pittsburgh/O=DTP Technologies, LLC/CN=${PROJECT_NAME} Root CA" + +# +# DEVELOPMENT CERTIFICATE +# + +# Create an OpenSSL configuration file for the development certificate +echo "Creating OpenSSL configuration file for development certificate..." +cat > ${PROJECT_DOMAIN}.cnf < { + describe('Backend Build', () => { + it('should have dist/web-app.js output', () => { + const distPath = path.join(ROOT_DIR, 'dist', 'web-app.js'); + expect(fs.existsSync(distPath)).toBe(true); + }); + }); + + describe('Frontend Build', () => { + it('should build frontend without errors', async () => { + const { stdout, stderr } = await execAsync('cd frontend && pnpm build', { + cwd: ROOT_DIR, + timeout: 120000, + }); + expect(stderr).toBe(''); + expect(stdout).toContain('built in'); + }, 120000); + + it('should have dist/client/index.html output', () => { + const indexPath = path.join(ROOT_DIR, 'dist', 'client', 'index.html'); + expect(fs.existsSync(indexPath)).toBe(true); + }); + + it('should have dist/client/assets output', () => { + const assetsDir = path.join(ROOT_DIR, 'dist', 'client', 'assets'); + expect(fs.existsSync(assetsDir)).toBe(true); + const jsFiles = fs.readdirSync(assetsDir).filter(f => f.endsWith('.js')); + const cssFiles = fs.readdirSync(assetsDir).filter(f => f.endsWith('.css')); + expect(jsFiles.length).toBeGreaterThan(0); + expect(cssFiles.length).toBeGreaterThan(0); + }); + }); + + describe('Configuration Tests', () => { + it('should have SSL certificates', () => { + const keyPath = path.join(ROOT_DIR, 'ssl', 'code-dev.g4dge7.com.key'); + const crtPath = path.join(ROOT_DIR, 'ssl', 'code-dev.g4dge7.com.crt'); + expect(fs.existsSync(keyPath)).toBe(true); + expect(fs.existsSync(crtPath)).toBe(true); + }); + + it('should have valid SSL key file', () => { + const keyPath = path.join(ROOT_DIR, 'ssl', 'code-dev.g4dge7.com.key'); + const content = fs.readFileSync(keyPath, 'utf-8'); + expect(content).toContain('-----BEGIN PRIVATE KEY-----'); + }); + + it('should have valid SSL certificate', () => { + const crtPath = path.join(ROOT_DIR, 'ssl', 'code-dev.g4dge7.com.crt'); + const content = fs.readFileSync(crtPath, 'utf-8'); + expect(content).toContain('-----BEGIN CERTIFICATE-----'); + }); + }); +}); + +describe('DTP Web Application Runtime Tests', () => { + let backendRunning = false; + let frontendRunning = false; + + afterAll(async () => { + // Clean up if we started processes (in real tests, you'd manage lifecycle differently) + }); + + describe('Backend Server', () => { + it('should respond on configured port', async () => { + // This test checks if the backend is reachable + // Note: In a real test, you'd start the backend process first + const options = { + hostname: 'localhost', + port: BACKEND_PORT, + method: 'GET', + path: '/', + }; + + return new Promise((resolve, reject) => { + const req = http.request(options, (res) => { + // Expect some response (will be 404 for / but proves server is running) + expect(res.statusCode).toBeDefined(); + resolve(); + }); + + req.on('error', (err) => { + // If connection refused, backend isn't running (this is expected for manual testing) + if (err.code === 'ECONNREFUSED') { + resolve(); + } else { + reject(err); + } + }); + + req.end(); + }); + }); + }); + + describe('Frontend Proxy Configuration', () => { + it('should have vite config with correct proxy settings', () => { + const viteConfigPath = path.join(ROOT_DIR, 'frontend', 'vite.config.ts'); + const content = fs.readFileSync(viteConfigPath, 'utf-8'); + + // Verify proxy targets backend port + expect(content).toContain(`target: 'http://localhost:${BACKEND_PORT}'`); + + // Verify Socket.io proxy with websockets + expect(content).toContain("'/socket.io'"); + expect(content).toContain('ws: true'); + + // Verify auth proxy + expect(content).toContain("'/auth'"); + + // Verify api proxy + expect(content).toContain("'/api'"); + }); + + it('should have vite config with HTTPS', () => { + const viteConfigPath = path.join(ROOT_DIR, 'frontend', 'vite.config.ts'); + const content = fs.readFileSync(viteConfigPath, 'utf-8'); + + expect(content).toContain('https:'); + expect(content).toContain('.key'); + expect(content).toContain('.crt'); + }); + + it('should have correct frontend port in config', () => { + const viteConfigPath = path.join(ROOT_DIR, 'frontend', 'vite.config.ts'); + const content = fs.readFileSync(viteConfigPath, 'utf-8'); + + expect(content).toContain(`port: ${FRONTEND_PORT}`); + }); + }); +}); + +describe('Environment Configuration', () => { + it('should have environment variables for HTTPS', () => { + const envPath = path.join(ROOT_DIR, 'src', 'config', 'env.ts'); + const content = fs.readFileSync(envPath, 'utf-8'); + + expect(content).toContain('https'); + expect(content).toContain('port: parseInt'); + expect(content).toContain('keyFile'); + expect(content).toContain('crtFile'); + }); + + it('should have frontend port configured', () => { + const envPath = path.join(ROOT_DIR, 'src', 'config', 'env.ts'); + const content = fs.readFileSync(envPath, 'utf-8'); + + expect(content).toContain('frontend'); + expect(content).toContain('port:'); + }); +}); + +describe('Dependencies', () => { + it('should have all required dependencies in root package.json', () => { + const pkgPath = path.join(ROOT_DIR, 'package.json'); + const pkgContent = fs.readFileSync(pkgPath, 'utf-8'); + const pkg = JSON.parse(pkgContent); + + // Backend dependencies + expect(pkg.dependencies).toHaveProperty('socket.io'); + expect(pkg.dependencies).toHaveProperty('express'); + expect(pkg.dependencies).toHaveProperty('express-session'); + + // Frontend dependencies + expect(pkg.dependencies).toHaveProperty('react'); + expect(pkg.dependencies).toHaveProperty('react-dom'); + + // Dev dependencies + expect(pkg.devDependencies).toHaveProperty('vite'); + expect(pkg.devDependencies).toHaveProperty('typescript'); + expect(pkg.devDependencies).toHaveProperty('vitest'); + }); +}); \ No newline at end of file diff --git a/gadget-code/tests/e2e/auth.test.ts b/gadget-code/tests/e2e/auth.test.ts new file mode 100644 index 0000000..815b6f6 --- /dev/null +++ b/gadget-code/tests/e2e/auth.test.ts @@ -0,0 +1,38 @@ +import { chromium, test, expect } from '@playwright/test'; + +test.describe('Authentication Flow', () => { + test('should sign in and show authenticated home', async ({ page }) => { + await page.goto('https://code-dev.g4dge7.com:5174/sign-in'); + await page.waitForLoadState('networkidle'); + + await page.fill('#email', 'rob@digitaltelepresence.com'); + await page.fill('#password', 'ionfrali'); + await page.click('button[type="submit"]'); + await page.waitForTimeout(3000); + + const userData = await page.evaluate(() => localStorage.getItem('dtp_user')); + expect(userData).toBeDefined(); + + const content = await page.content(); + expect(content).toContain('Welcome'); + expect(content).toContain('Rob Colbert'); + }); + + test('should sign out and show unauthenticated home', async ({ page }) => { + await page.goto('https://code-dev.g4dge7.com:5174/sign-in'); + await page.waitForLoadState('networkidle'); + await page.fill('#email', 'rob@digitaltelepresence.com'); + await page.fill('#password', 'ionfrali'); + await page.click('button[type="submit"]'); + await page.waitForTimeout(2000); + + await page.click('button:has-text("Sign Out")'); + await page.waitForTimeout(2000); + + const userData = await page.evaluate(() => localStorage.getItem('dtp_user')); + expect(userData).toBeNull(); + + const content = await page.content(); + expect(content).toContain('Sign Up Today!'); + }); +}); \ No newline at end of file diff --git a/gadget-code/tests/e2e/manual-test.ts b/gadget-code/tests/e2e/manual-test.ts new file mode 100644 index 0000000..6741d23 --- /dev/null +++ b/gadget-code/tests/e2e/manual-test.ts @@ -0,0 +1,67 @@ +import { test, expect, chromium } from '@playwright/test'; + +async function runTest() { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + ignoreHTTPSErrors: true, + }); + const page = await context.newPage(); + + page.on('console', msg => console.log('BROWSER:', msg.text())); + + try { + await page.goto('https://code-dev.g4dge7.com:5174/sign-in', { + ignoreHTTPSErrors: true, + }); + await page.waitForLoadState('networkidle'); + + console.log('Filling form...'); + await page.fill('#email', 'rob@digitaltelepresence.com'); + await page.fill('#password', 'ionfrali'); + + // Also evaluate directly in page context to see what's happening + await page.evaluate(() => { + console.log('Testing fetch directly...'); + fetch('/auth/sign-in', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + email: 'rob@digitaltelepresence.com', + password: 'ionfrali' + }) + }).then(r => r.json()).then(d => { + console.log('Direct fetch result:', JSON.stringify(d)); + }); + }); + + console.log('Clicking submit...'); + await page.click('button[type="submit"]'); + + // Wait and check + await page.waitForTimeout(3000); + + const userData = await page.evaluate(() => localStorage.getItem('dtp_user')); + const tokenData = await page.evaluate(() => localStorage.getItem('dtp_auth_token')); + + console.log('=== RESULTS ==='); + console.log('User in localStorage:', userData ? 'YES' : 'NO'); + console.log('Token in localStorage:', tokenData ? 'YES' : 'NO'); + + const content = await page.content(); + const welcomeVisible = content.includes('Welcome'); + console.log('Welcome visible:', welcomeVisible); + + if (welcomeVisible) { + console.log('\n✅ PASSED'); + } else { + console.log('\n❌ FAILED'); + } + + } catch (e) { + console.error('Error:', e.message); + } finally { + await browser.close(); + } +} + +runTest(); \ No newline at end of file diff --git a/gadget-code/tsconfig.json b/gadget-code/tsconfig.json new file mode 100644 index 0000000..860528e --- /dev/null +++ b/gadget-code/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["es2022", "dom", "dom.iterable"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "module": "ESNext", + "rootDir": "./src", + "paths": { + "@/*": ["./src/*"] + }, + "moduleResolution": "bundler", + "typeRoots": ["node_modules/@types", "types"], + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "removeComments": false, + "importHelpers": true, + "sourceRoot": "./src", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "pretty": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["data", "node_modules"], + "files": [ + "types/error.d.ts", + "types/express-session.d.ts", + "types/express.d.ts", + "types/window.d.ts" + ] +} diff --git a/gadget-code/types/error.d.ts b/gadget-code/types/error.d.ts new file mode 100644 index 0000000..2e46fcc --- /dev/null +++ b/gadget-code/types/error.d.ts @@ -0,0 +1,7 @@ +// types/error.d.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +interface Error { + statusCode?: number; +} diff --git a/gadget-code/types/express-session.d.ts b/gadget-code/types/express-session.d.ts new file mode 100644 index 0000000..c666b53 --- /dev/null +++ b/gadget-code/types/express-session.d.ts @@ -0,0 +1,18 @@ +// types/express-session.d.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { SessionData } from "express-session"; +import { IUser } from "../models/user.js"; + +declare module "express-session" { + interface SessionData { + type?: string; + user?: IUser | null; + token?: string; + captcha: { + [key: string]: string; + // add custom session vars here + }; + } +} diff --git a/gadget-code/types/express.d.ts b/gadget-code/types/express.d.ts new file mode 100644 index 0000000..49c74de --- /dev/null +++ b/gadget-code/types/express.d.ts @@ -0,0 +1,26 @@ +// types/express.d.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { IUser } from "../src/models/user.js"; + +declare global { + namespace Express { + interface Locals { + user?: IUser | null; + // define custom locals properties here + } + + interface Request { + rawBody?: string; + user?: IUser | null; + // define custom request properties here + } + + // req.params + interface ParamsDictionary { + [key: string]: unknown; + // add custom parameters here + } + } +} diff --git a/gadget-code/types/window.d.ts b/gadget-code/types/window.d.ts new file mode 100644 index 0000000..f36f047 --- /dev/null +++ b/gadget-code/types/window.d.ts @@ -0,0 +1,17 @@ +// types/window.d.ts +// Copyright (C) 2026 Robert Colbert +// All Rights Reserved + +import { DtpGlobals } from "client/js/lib/globals.ts"; +import { DtpWebApp } from "../src/client/js/web-app.ts"; + +declare global { + interface Window { + dtp: DtpGlobals; + app: DtpWebApp; + } + interface GlobalThis extends Window { + dtp: DtpGlobals; + app: DtpWebApp; + } +} diff --git a/gadget-code/vitest.config.ts b/gadget-code/vitest.config.ts new file mode 100644 index 0000000..1586d5b --- /dev/null +++ b/gadget-code/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts', '!tests/e2e/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + }, + }, +}); \ No newline at end of file diff --git a/gadget-drone/.gitignore b/gadget-drone/.gitignore new file mode 100644 index 0000000..1bf742c --- /dev/null +++ b/gadget-drone/.gitignore @@ -0,0 +1,6 @@ +.env + +dist +logs + +node_modules \ No newline at end of file diff --git a/gadget-drone/.vscode/launch.json b/gadget-drone/.vscode/launch.json new file mode 100644 index 0000000..7243e73 --- /dev/null +++ b/gadget-drone/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/dist/gadget-drone.js", + "preLaunchTask": "tsc: build - tsconfig.json", + "console": "integratedTerminal", + "outFiles": ["${workspaceFolder}/dist/**/*.js"] + } + ] +} diff --git a/gadget-drone/AGENTS.md b/gadget-drone/AGENTS.md new file mode 100644 index 0000000..6f19db0 --- /dev/null +++ b/gadget-drone/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS.md + +## Dev Commands + +```bash +pnpm dev # Run with tsx (src/gadget-drone.ts) +pnpm build # Compile TypeScript to dist/ +pnpm start # Run built code (dist/gadget-drone.js) +``` + +## Requirements + +- **Redis** must be running (configure via `GADGET_REDIS_HOST`, `GADGET_REDIS_PORT`) +- **Platform credentials** required in `.env`: + - `GADGET_PLATFORM_URL` + - `GADGET_PLATFORM_KEY` + +## Architecture + +- **Entry**: `src/gadget-drone.ts` - extends `GadgetProcess`, registers with platform, attaches to Bull queue +- **Services**: `AgentService`, `PlatformService` in `src/services/` +- **Queue**: Bull queue named `gadget-drone`, job type `prompt` + +## Build + +`tsc` with `module: NodeNext` outputs to `dist/`. ES modules only. + +## Tests + +None configured (`pnpm test` exits with error). \ No newline at end of file diff --git a/gadget-drone/LICENSE b/gadget-drone/LICENSE new file mode 100644 index 0000000..d741e4c --- /dev/null +++ b/gadget-drone/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, + the Licensor for the purpose of discussing and improving the Work, + but excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + where such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR + PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claimed as a result of, Your acceptance of such + obligations or act on Your behalf. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/gadget-drone/docs/agentic-workflow-loop.md b/gadget-drone/docs/agentic-workflow-loop.md new file mode 100644 index 0000000..2286154 --- /dev/null +++ b/gadget-drone/docs/agentic-workflow-loop.md @@ -0,0 +1,126 @@ +# Gadget Drone: The Agentic Workflow Loop + +[Gadget Drone](./gadget-drone.md) exists primarily to run the Agentic Workflow Loop (AWL) within the Gadget Code ecosystem. + +The User is using a chat interface in an HTML5 web application connected to the Gadget Code web server using Socket.IO. Gadget Drone is also connected to the Gadget Code web server using Socket.IO. The User submits prompts for processing in the chat interface, and the web server packages them as Work Order jobs for Gadget Drone to process. + +As the drone is calling AI APIs to have the chat processed and receiving streaming responses, it emits messages to the web server to update the User's chat session context and the AI service provider's statistics. + +## The Work Order + +Each Gadget Drone registered by the User implements a named Bull job queue. Gadget Code will create a Work Order job for the Drone to process, and place it into a selected drone's job queue for processing. + +The Work Order is a JSON object that contains information about the project, AI service provider and model, and also the User's prompt. + +````typescript +/* + * Chat session context is made of chat messages with a timestamp and role. The + * role is "system", "user", "assistant", or "tool". The assistant can have + * reasoning or "thinking" content. When thinking content is being included, we + * merge the thinking + response by enclosing the thinking content in a + * element in the content. + * + * ``` + * I need to research the project... + * I found the bug! + * ``` + */ +interface IChatMessage { + createdAt: Date; + role: string; + content: string; +} + +interface IWorkOrder { + project: { + _id: string; + name: string; + slug: string; + gitUrl: string; + }; + provider: { + sdk: "ollama" | "openai"; + baseUrl: string; + apiKey: string; + modelId: string; + params: { + temperature: number; + topP: number; + topK: number; + }; + }; + chatSession: { + _id: string; + name: string; + context: IChatMessage[]; + }; + chatTurn: { + _id: string; + mode: string; + prompt: string; + }; +} +```` + +## The Workflow Loop + +When a Work Order job is received a Gadget Drone, it performs a series of steps in preparation to work on the prompt contained in the work order. + +1. Check if the workspace directory currently has the project cloned. If not, clone the project from git into the workspace directory. + +2. Instantiate the correct AI API client object configured with the credentials provided in the work order. + +```typescript +/** + * Called when the drone receives a Work Order job for processing. The Gadget + * Code web server will have already created the `ChatTurn` within the + * `ChatSession` happening within the `Project`, and is passing all information + * needed by the drone to execute the prompt. + */ +async function onWorkOrder(job: Bull.Job): Promise { + // this turn's context (system, history, prompt, work) + const messages = []; + + const systemPrompt = buildSystemPrompt(session); + messages.push({ role: "system", content: systemPrompt }); + + // recall full session history into messages array + buildSessionContext(job, messages); + + // push the User's latest prompt to the context + messages.push({ role: "user", content: job.data.prompt }); + + let keepProcessing = true; + do { + const response = await aiApiCall(messages); + keeProcessing = response.tool_calls.length > 0; + for (const tool_call of response.tool_calls) { + const response = await callTool(tool_call.name, tool_call.args); + messages.push({ role: "tool", content: response }); + /* emit turn-tool-call socket message */ + } + } while (keepProcessing); + + /* emit turn-finished socket message */ +} + +function buildSessionContext( + job: Bull.Job, + messages: IChatMessage[], +): void { + for (const message of job.data.chatSession.context) { + if (message.role === "system") { + continue; // we don't + } + messages.push(message); + } +} + +/** + * To optimize context, reduce clutter, and help the agent focus, full outputs + * of older file reads and edits are summarized once a newer version is available. + */ +function pruneSessionContext(messages: IChatMessage[]): void { + // TODO +} +``` diff --git a/gadget-drone/docs/gadget-drone.md b/gadget-drone/docs/gadget-drone.md new file mode 100644 index 0000000..7eb176a --- /dev/null +++ b/gadget-drone/docs/gadget-drone.md @@ -0,0 +1,55 @@ +# Gadget Drone + +Gadget Drone is a worker component in the Gadget Code ecosystem. It is built and maintained separately from this project. Gadget Drone operates as a standalone process running on a host in a directory of the User's choosing. + +The User can operate more than one Gadget Drone per host by running them in separate Gadget Drone Workspace Directories. + +## Workspace Directory + +A Gadget Drone Workspace Directory is simply `process.cwd()` at startup. It is the directory in which the drone has been started. The drone will manage the contents of the directory, and expects the directory to be empty the first time the drone is started. + +If the workspace directory isn't empty at startup, and the directory doesn't contain the `.gadget` subdirectory with a stored `.registration.json`, it refuses to start. + +If the directory is already a workspace directory, the drone reads the `.registration.json` file and uses that information to connect to the Gadget Code web service. + +## Startup Procedure + +At startup, Gadget Drone: + +1. Attempts to take ownership of process.cwd() as a Gadget Drone Workspace Directory. A workspace will contain clones of git repositories stored in directories matching the `Project.slug`. + + , and verifies that: + 1. The directory is already a workspace directory; or + 2. Empty. + + If not a Gadget Drone directory, and if also not empty, Gadget Drone refuses to start with a message printed to the console, and terminates. + +2. Creates the `.gadget` and `.gadget-cache` directories in the directory on launch if they are not there. + +It authenticates to the Gadget Code web service, receives a `DroneRegistration` and `DroneSocketSession`, connects to the Socket.IO session, and waits for commands. + +Commands are sent from the Gadget Code web server to the Gadget Drone process as a Socket message. The drone processes the message, and will emit status and update events as it works. These status and update events are sent to the Gadget Code web service, where they are processed into a `ChatHistory` record as part of a `ChatSession` being operated by the User in an HTML5 browser client. + +## `ChatSession` and `ChatHistory` + +The User will enter prompts to be processed by the Gadget Code Agent, which runs in Gadget Done. One message the system sends to Gadget Drone is `prompt`, which is a message containing all details needed to execute a prompt, such as the project in which the prompt is being executed, and other details. + +The drone then executes the prompt in the project's directory to work on the project, emitting updates as it streams the response from the AI API in use, and sending a `prompt-finished` message upon completion to close out the `ChatSession` turn. + +A `ChatSession` turn equals one `ChatHistory` record in the database, and is made of: + +1. The User's input prompt +2. The Agent's Thinking content +3. The Agent's Response content +4. All tool calls made by the Agent, to include: + 1. The name of the tool function being called + 2. The input parameters to the tool call + 3. The response from the tool call + +Gadget Drone: + +## `.gadget` directory + +When the Gadget Drone starts and initializes, + +## `.gadget-cache` directory diff --git a/gadget-drone/gadget-drone.code-workspace b/gadget-drone/gadget-drone.code-workspace new file mode 100644 index 0000000..e529cc2 --- /dev/null +++ b/gadget-drone/gadget-drone.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": ".", + }, + ], + "settings": {}, +} diff --git a/gadget-drone/package.json b/gadget-drone/package.json new file mode 100644 index 0000000..f9f7154 --- /dev/null +++ b/gadget-drone/package.json @@ -0,0 +1,40 @@ +{ + "name": "@gadget/drone", + "version": "1.0.0", + "description": "Gadget Code drone process", + "type": "module", + "main": "./dist/gadget-drone.js", + "scripts": { + "dev": "tsx src/gadget-drone.ts", + "dev:watch": "tsx watch src/gadget-drone.ts", + "build": "tsc", + "start": "node dist/gadget-drone.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "gadget", + "drone", + "worker" + ], + "author": "Rob Colbert", + "license": "Apache-2.0", + "packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8", + "dependencies": { + "@gadget/ai": "workspace:*", + "ansicolor": "^2.0.3", + "dayjs": "^1.11.20", + "dotenv": "^17.4.2", + "numeral": "^2.0.6", + "ollama": "^0.6.3", + "openai": "^6.34.0", + "socket.io-client": "^4.8.3" + }, + "devDependencies": { + "@types/node": "^25.6.0", + "@types/numeral": "^2.0.5", + "prettier": "^3.8.3", + "tsc-alias": "^1.8.16", + "tsx": "^4.21.0", + "typescript": "^6.0.3" + } +} diff --git a/gadget-drone/pnpm-lock.yaml b/gadget-drone/pnpm-lock.yaml new file mode 100644 index 0000000..47740e0 --- /dev/null +++ b/gadget-drone/pnpm-lock.yaml @@ -0,0 +1,702 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + ansicolor: + specifier: ^2.0.3 + version: 2.0.3 + dayjs: + specifier: ^1.11.20 + version: 1.11.20 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + numeral: + specifier: ^2.0.6 + version: 2.0.6 + ollama: + specifier: ^0.6.3 + version: 0.6.3 + openai: + specifier: ^6.34.0 + version: 6.34.0 + devDependencies: + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 + prettier: + specifier: ^3.8.3 + version: 3.8.3 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + +packages: + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/numeral@2.0.5': + resolution: {integrity: sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==} + + ansicolor@2.0.3: + resolution: {integrity: sha512-pzusTqk9VHrjgMCcTPDTTvfJfx6Q3+L5tQ6yKC8Diexmoit4YROTFIkxFvRTNL9y5s0Q8HrSrgerCD5bIC+Kiw==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mylas@2.1.14: + resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} + engines: {node: '>=16.0.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + numeral@2.0.6: + resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} + + ollama@0.6.3: + resolution: {integrity: sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==} + + openai@6.34.0: + resolution: {integrity: sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} + hasBin: true + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + + '@types/numeral@2.0.5': {} + + ansicolor@2.0.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + array-union@2.1.0: {} + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + commander@9.5.0: {} + + dayjs@1.11.20: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dotenv@17.4.2: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + ignore@5.3.2: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mylas@2.1.14: {} + + normalize-path@3.0.0: {} + + numeral@2.0.6: {} + + ollama@0.6.3: + dependencies: + whatwg-fetch: 3.6.20 + + openai@6.34.0: {} + + path-type@4.0.0: {} + + picomatch@2.3.2: {} + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + + prettier@3.8.3: {} + + queue-lit@1.5.2: {} + + queue-microtask@1.2.3: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + resolve-pkg-maps@1.0.0: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + slash@3.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tsc-alias@1.8.16: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.14.0 + globby: 11.1.0 + mylas: 2.1.14 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + + typescript@6.0.3: {} + + undici-types@7.19.2: {} + + whatwg-fetch@3.6.20: {} diff --git a/gadget-drone/pnpm-workspace.yaml b/gadget-drone/pnpm-workspace.yaml new file mode 100644 index 0000000..10188a9 --- /dev/null +++ b/gadget-drone/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + msgpackr-extract: true diff --git a/gadget-drone/src/config/env.ts b/gadget-drone/src/config/env.ts new file mode 100644 index 0000000..127acb1 --- /dev/null +++ b/gadget-drone/src/config/env.ts @@ -0,0 +1,73 @@ +// config/env.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import "dotenv/config"; +import type PackageJson from "../../package.json"; + +import assert from "node:assert"; + +import path, { dirname } from "node:path"; +import fs from "node:fs"; +import { fileURLToPath } from "node:url"; +const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line + +assert( + process.env.GADGET_PLATFORM_URL, + "must define GADGET_PLATFORM_URL in .env", +); +assert( + process.env.GADGET_PLATFORM_KEY, + "must define GADGET_PLATFORM_KEY in .env", +); + +const ROOT_DIR = path.resolve(__dirname, "..", ".."); +const SRC_DIR = path.resolve(__dirname, ".."); + +async function readJsonFile(path: string): Promise { + const file = await fs.promises.readFile(path); + return JSON.parse(file.toString("utf-8")) as T; +} + +/* eslint-disable no-process-env */ +export default { + NODE_ENV: process.env.NODE_ENV, + timezone: process.env.GADGET_TIMEZONE || "America/New_York", + root: ROOT_DIR, + src: SRC_DIR, + pkg: await readJsonFile( + path.join(ROOT_DIR, "package.json"), + ), + platform: { + baseUrl: process.env.GADGET_PLATFORM_URL, + apiKey: process.env.GADGET_PLATFORM_KEY, + }, + redis: { + host: process.env.GADGET_REDIS_HOST || "localhost", + port: parseInt(process.env.GADGET_REDIS_PORT || "6379", 10), + password: process.env.GADGET_REDIS_PASSWORD, + keyPrefix: process.env.GADGET_REDIS_KEY_PREFIX || "dnews", + lazyConnect: process.env.GADGET_REDIS_LAZYCONNECT === "enabled", + }, + log: { + console: { + enabled: process.env.GADGET_LOG_CONSOLE === "enabled", + }, + file: { + enabled: process.env.GADGET_LOG_FILE === "enabled", + path: process.env.GADGET_LOG_FILE_PATH || path.join(ROOT_DIR, "logs"), + name: process.env.GADGET_LOG_FILE_NAME || "gadget-drone", + maxWritesPerFile: parseInt( + process.env.GADGET_LOG_FILE_MAX_WRITES || "10000", + 10, + ), + maxFiles: parseInt(process.env.GADGET_LOG_FILE_MAX_FILES || "10", 10), + }, + levels: { + debug: process.env.GADGET_LOG_DEBUG === "enabled", + info: process.env.GADGET_LOG_INFO === "enabled", + warn: process.env.GADGET_LOG_WARN === "enabled", + }, + }, +}; +/* eslint-enable no-process-env */ diff --git a/gadget-drone/src/gadget-drone.ts b/gadget-drone/src/gadget-drone.ts new file mode 100644 index 0000000..d8317dc --- /dev/null +++ b/gadget-drone/src/gadget-drone.ts @@ -0,0 +1,172 @@ +// src/gadget-drone.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import env from "./config/env.ts"; +import assert from "node:assert"; + +import { io, ManagerOptions, SocketOptions, Socket } from "socket.io-client"; + +import AgentService, { IAgentWorkOrder } from "./services/agent.ts"; +import AiService from "./services/ai.ts"; +import PlatformService, { + DroneStatus, + PlatformRegistration, +} from "./services/platform.ts"; + +import { GadgetProcess } from "./lib/process.ts"; + +class GadgetDrone extends GadgetProcess { + private registration: PlatformRegistration | undefined; + private socket: Socket | undefined; + private isShuttingDown: boolean = false; + + get name(): string { + return "GadgetDrone"; + } + + get slug(): string { + return "gadget-drone"; + } + + constructor() { + super(); + } + + async start(): Promise { + /* + * Initialize the system + */ + + this.hookProcessSignals(); + await this.startServices(); + + /* + * Register this Drone with the Gadget Code web services platform. + */ + + const email = "rob@digitaltelepresence.com"; + const password = "ionfrali"; + this.registration = await PlatformService.register(email, password); + this.log.info("registered with platform", { + registration: this.registration, + }); + + /* + * Connect to the Gadget Code web services platform and configure the real- + * time messaging system on Socket.IO. + */ + await this.connectSocket(); + + /* + * Mark this Drone as available and ready to accept work orders. + */ + + await PlatformService.setStatus(DroneStatus.Available); + this.log.info(`Gadget Drone v${env.pkg.version} started`); + } + + async stop(): Promise { + this.log.info(`Gadget Drone v${env.pkg.version} shutting down`); + + if (this.socket) { + this.socket.disconnect(); + delete this.socket; + } + + await PlatformService.unregister(); + await this.stopServices(); + + return 0; + } + + async startServices(): Promise { + this.log.info("starting services"); + + await AgentService.start(); + await AiService.start(); + await PlatformService.start(); + + this.log.info("services started"); + } + + async stopServices(): Promise { + this.log.info("stopping services"); + + await AgentService.stop(); + await AiService.stop(); + await PlatformService.stop(); + + this.log.info("services stopped"); + } + + async connectSocket(): Promise { + return new Promise((resolve, reject) => { + assert(this.registration, "must be registered with Gadget Code platform"); + const options: Partial = { + auth: { token: this.registration._id }, + reconnectionAttempts: 10, + timeout: 5000, + transports: ["websocket"], + }; + + /* + * Allow self-signed certs in non-production environments + */ + if (env.NODE_ENV !== "production") { + options.rejectUnauthorized = false; + } + + this.log.debug("connecting to Gadget Code platform..."); + this.socket = io("https://code-dev.g4dge7.com:5174/", options); + this.socket.on("connect_error", (err) => { + this.log.error("socket connect error", { err }); + reject(err); + }); + this.socket.on("connect", () => { + this.log.info("connected to Gadget Code platform."); + resolve(); + }); + }); + } + + hookProcessSignals(): void { + process.title = this.name; + + process.on("unhandledRejection", async (error: Error, p) => { + this.log.error("Unhandled rejection", { + error, + promise: p, + stack: error.stack, + }); + + const exitCode = await this.stop(); + process.exit(exitCode); + }); + + process.on("warning", (error) => { + if (error.name === "DeprecationWarning") return; + this.log.alert("warning", { error }); + }); + + process.on("SIGINT", async () => { + this.log.info("SIGINT received"); + if (this.isShuttingDown) return; + + this.log.info("requesting shutdown"); + const exitCode = await this.stop(); + + process.exit(exitCode); + }); + } +} + +(async () => { + try { + const drone = new GadgetDrone(); + await drone.start(); + } catch (error) { + console.error("failed to start gadget-drone", error); + process.exit(-1); + } +})(); diff --git a/gadget-drone/src/lib/component.ts b/gadget-drone/src/lib/component.ts new file mode 100644 index 0000000..4aa05d0 --- /dev/null +++ b/gadget-drone/src/lib/component.ts @@ -0,0 +1,8 @@ +// src/lib/component.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +export interface GadgetComponent { + get name(): string; + get slug(): string; +} diff --git a/gadget-drone/src/lib/log-file.ts b/gadget-drone/src/lib/log-file.ts new file mode 100644 index 0000000..f4f88da --- /dev/null +++ b/gadget-drone/src/lib/log-file.ts @@ -0,0 +1,73 @@ +// src/lib/log-file.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import env from "../config/env.js"; + +import fs from "node:fs"; +import path from "node:path"; + +import numeral from "numeral"; + +import { Writable, WritableOptions } from "stream"; + +type StreamCallback = (error?: Error | null) => void; + +export interface GadgetLogFileOptions extends WritableOptions { + basePath: string; + name: string; + maxWritesPerFile: number; + maxFiles: number; +} + +export class GadgetLogFile extends Writable { + options: GadgetLogFileOptions; + file?: fs.WriteStream; + + fileIdx: number = 0; + writeCount: number = 0; + + constructor(options: GadgetLogFileOptions) { + super(options); + this.options = options; + } + + open(): void { + fs.mkdirSync(this.options.basePath, { recursive: true }); + const filename = path.join( + this.options.basePath, + `${this.options.name}.${numeral(this.fileIdx).format("000")}.log`, + ); + this.file = fs.createWriteStream(filename, { encoding: "utf-8" }); + this.writeCount = 0; + } + + _write( + chunk: unknown, + encoding: BufferEncoding, + callback: StreamCallback, + ): boolean { + if (!this.file) { + return false; + } + if (this.writeCount > this.options.maxWritesPerFile) { + this.file.close(); + if (++this.fileIdx > this.options.maxFiles) { + this.fileIdx = 0; + } + this.open(); + } + return this.file.write(chunk, encoding, callback); + } +} + +const defaultLogFileOptions = { + basePath: env.log.file.path || "/var/log/gadget-code", + name: env.log.file.name || env.pkg.name, + maxWritesPerFile: env.log.file.maxWritesPerFile, + maxFiles: env.log.file.maxFiles, +}; +const defaultLogFile = new GadgetLogFile(defaultLogFileOptions); +defaultLogFile.open(); + +export { defaultLogFile }; diff --git a/gadget-drone/src/lib/log-transport-console.ts b/gadget-drone/src/lib/log-transport-console.ts new file mode 100644 index 0000000..9c65740 --- /dev/null +++ b/gadget-drone/src/lib/log-transport-console.ts @@ -0,0 +1,62 @@ +// src/lib/log-transport-console.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import * as util from "node:util"; + +import dayjs from "dayjs"; +import color from "ansicolor"; + +import { GadgetLogLevel } from "./log.js"; +import { GadgetLogTransport } from "./log-transport.js"; +import { GadgetComponent } from "./component.ts"; + +export class GadgetLogTransportConsole implements GadgetLogTransport { + async writeLog( + timestamp: Date, + component: GadgetComponent, + level: GadgetLogLevel, + message: string, + metadata?: unknown, + ): Promise { + let clevel = level.padEnd(5); + switch (level) { + case "debug": + clevel = color.darkGray(clevel); + break; + case "info": + clevel = color.green(clevel); + break; + case "warn": + clevel = color.yellow(clevel); + break; + case "alert": + clevel = color.red(clevel); + break; + case "error": + clevel = color.bgRed.white(clevel); + break; + case "crit": + clevel = color.bgRed.yellow(clevel); + break; + case "fatal": + clevel = color.bgRed.darkGray(clevel); + break; + } + + const ccomponent = color.cyan(component.name); + const ctimestamp = color.darkGray( + dayjs(timestamp).format("YYYY-MM-DD HH:mm:ss.SSS"), + ); + const cmessage = color.darkGray(message); + + if (metadata) { + console.log( + `${ctimestamp} ${clevel} ${ccomponent} ${cmessage}`, + util.inspect(metadata, false, Infinity, true), + ); + } else { + console.log(`${ctimestamp} ${clevel} ${ccomponent} ${cmessage}`); + } + } +} diff --git a/gadget-drone/src/lib/log-transport-file.ts b/gadget-drone/src/lib/log-transport-file.ts new file mode 100644 index 0000000..10576d9 --- /dev/null +++ b/gadget-drone/src/lib/log-transport-file.ts @@ -0,0 +1,53 @@ +// src/lib/log-transport-file.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import util from "node:util"; +import { Writable } from "node:stream"; + +import dayjs from "dayjs"; + +import { GadgetLogLevel } from "./log.js"; +import { GadgetLogTransport } from "./log-transport.js"; +import { GadgetComponent } from "./component.ts"; + +interface LogFilePayload { + timestamp: Date; + component: string; + level: GadgetLogLevel; + message: string; + metadata?: unknown; +} + +export class GadgetLogTransportFile implements GadgetLogTransport { + file: Writable; + + constructor(file: Writable) { + this.file = file; + } + + async writeLog( + timestamp: Date, + component: GadgetComponent, + level: GadgetLogLevel, + message: string, + metadata?: unknown, + ): Promise { + return new Promise((resolve, reject) => { + const stimestamp = dayjs(timestamp).format("YYYY-MM-DD HH:mm:ss.SSS"); + + let sLogMessage = `${stimestamp} ${level} ${component.slug} ${message}`; + if (metadata) { + sLogMessage += " " + util.inspect(metadata, false, Infinity, false); + } + + const chunk = Buffer.from(sLogMessage + "\n"); + this.file.write(chunk, (error: Error | null | undefined): void => { + if (error) { + return reject(error); + } + return resolve(); + }); + }); + } +} diff --git a/gadget-drone/src/lib/log-transport.ts b/gadget-drone/src/lib/log-transport.ts new file mode 100644 index 0000000..acb324f --- /dev/null +++ b/gadget-drone/src/lib/log-transport.ts @@ -0,0 +1,16 @@ +// src/lib/log-transport.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import { GadgetComponent } from "./component.ts"; +import { GadgetLogLevel } from "./log.js"; + +export abstract class GadgetLogTransport { + abstract writeLog( + timestamp: Date, + component: GadgetComponent, + level: GadgetLogLevel, + message: string, + metadata?: unknown, + ): Promise; +} diff --git a/gadget-drone/src/lib/log.ts b/gadget-drone/src/lib/log.ts new file mode 100644 index 0000000..33306b2 --- /dev/null +++ b/gadget-drone/src/lib/log.ts @@ -0,0 +1,101 @@ +// src/lib/log.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import env from "../config/env.js"; + +import { GadgetComponent } from "./component.ts"; +import { GadgetLogTransportConsole } from "./log-transport-console.js"; +import { GadgetLogTransportFile } from "./log-transport-file.js"; +import { GadgetLogTransport } from "./log-transport.js"; +import { GadgetLogFile, defaultLogFile } from "./log-file.js"; + +export enum GadgetLogLevel { + debug = "debug", + info = "info", + warn = "warn", + alert = "alert", + error = "error", + crit = "crit", + fatal = "fatal", +} + +export class GadgetLog { + component: GadgetComponent; + transports: GadgetLogTransport[] = []; + + private static defaultTransports: GadgetLogTransport[] = []; + private file: GadgetLogFile; + + constructor(component: GadgetComponent, file?: GadgetLogFile) { + this.component = component; + this.file = file || defaultLogFile; + this.transports.push(...GadgetLog.defaultTransports); + + if (env.log.console.enabled) { + this.transports.push(new GadgetLogTransportConsole()); + } + if (env.log.file.enabled) { + this.transports.push(new GadgetLogTransportFile(this.file)); + } + } + + public static addDefaultTransport(transport: GadgetLogTransport): void { + GadgetLog.defaultTransports.push(transport); + } + + public static removeDefaultTransport(transport: GadgetLogTransport): void { + const index = GadgetLog.defaultTransports.indexOf(transport); + if (index > -1) { + GadgetLog.defaultTransports.splice(index, 1); + } + } + + public static getDefaultTransports(): GadgetLogTransport[] { + return GadgetLog.defaultTransports; + } + + async debug(message: string, metadata?: unknown) { + if (!env.log.levels.debug) { + return; + } + return this.writeLog(GadgetLogLevel.debug, message, metadata); + } + + async info(message: string, metadata?: unknown) { + if (!env.log.levels.info) { + return; + } + return this.writeLog(GadgetLogLevel.info, message, metadata); + } + + async warn(message: string, metadata?: unknown) { + if (!env.log.levels.warn) { + return; + } + return this.writeLog(GadgetLogLevel.warn, message, metadata); + } + + async alert(message: string, metadata?: unknown) { + return this.writeLog(GadgetLogLevel.alert, message, metadata); + } + + async error(message: string, metadata?: unknown) { + return this.writeLog(GadgetLogLevel.error, message, metadata); + } + + async crit(message: string, metadata?: unknown) { + return this.writeLog(GadgetLogLevel.crit, message, metadata); + } + + async fatal(message: string, metadata?: unknown) { + this.writeLog(GadgetLogLevel.fatal, message, metadata); + } + + async writeLog(level: GadgetLogLevel, message: string, metadata?: unknown) { + const NOW = new Date(); + for (const transport of this.transports) { + transport.writeLog(NOW, this.component, level, message, metadata); + } + } +} diff --git a/gadget-drone/src/lib/process.ts b/gadget-drone/src/lib/process.ts new file mode 100644 index 0000000..24a9501 --- /dev/null +++ b/gadget-drone/src/lib/process.ts @@ -0,0 +1,20 @@ +// src/lib/process.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import { GadgetComponent } from "./component.ts"; +import { GadgetLog } from "./log.ts"; + +export abstract class GadgetProcess implements GadgetComponent { + protected log: GadgetLog; + + constructor() { + this.log = new GadgetLog(this); + } + + abstract get name(): string; + abstract get slug(): string; + + abstract start(): Promise; + abstract stop(): Promise; +} diff --git a/gadget-drone/src/lib/service.ts b/gadget-drone/src/lib/service.ts new file mode 100644 index 0000000..83ae137 --- /dev/null +++ b/gadget-drone/src/lib/service.ts @@ -0,0 +1,22 @@ +// src/lib/service.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +// import env from "../config/env.js"; + +import { GadgetComponent } from "./component.js"; +import { GadgetLog } from "./log.js"; + +export abstract class GadgetService implements GadgetComponent { + protected log: GadgetLog; + + constructor() { + this.log = new GadgetLog(this); + } + + abstract get name(): string; + abstract get slug(): string; + + abstract start(): Promise; + abstract stop(): Promise; +} diff --git a/gadget-drone/src/services/agent.ts b/gadget-drone/src/services/agent.ts new file mode 100644 index 0000000..b212aff --- /dev/null +++ b/gadget-drone/src/services/agent.ts @@ -0,0 +1,152 @@ +// src/services/agent.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import assert from "node:assert"; +import path from "node:path"; +import fs from "node:fs"; + +import dayjs from "dayjs"; + +import { type IAiProvider, type IContextChatMessage } from "@gadget/ai"; + +import AiService from "./ai.ts"; + +import { GadgetService } from "../lib/service.ts"; + +export interface IProject { + _id: string; + name: string; + slug: string; + gitUrl: string; +} + +export interface IToolCall { + name: string; + params: string; + call_id?: string; + response?: string; + error?: Error; +} + +export interface IChatMessage { + role: string; + content: string; +} + +export interface IChatTurn { + _id: string; + mode: string; + modelId?: string; + prompts: { + system: string; + user: string; + }; +} + +export interface IChatSession { + _id: string; + name: string; + context: IContextChatMessage[]; +} + +export interface IAgentWorkOrder { + project: IProject; + provider: IAiProvider; + session: IChatSession; + turn: IChatTurn; +} + +class AgentService extends GadgetService { + get name(): string { + return "AgentService"; + } + get slug(): string { + return "svc:agent"; + } + + async start(): Promise { + this.log.info("started"); + } + + async stop(): Promise { + this.log.info("stopped"); + } + + async process(workOrder: IAgentWorkOrder): Promise { + const { project, provider, session, turn } = workOrder; + + async function aiCallTool(name: string, args: string) { + return "[all tool calls are stubbed out]"; + } + + // this turn's context (system, history, prompt, work) + const messages: IChatMessage[] = []; + + messages.push({ role: "system", content: turn.prompts.system }); + + // recall full session history into messages array + this.buildSessionContext(session, messages); + + // push the User's latest prompt to the context + messages.push({ role: "user", content: workOrder.turn.prompts.user }); + + const modelConfig = { + provider: workOrder.provider, + modelId: workOrder.turn.modelId ?? workOrder.provider.defaultModelId ?? "llama3.2", + params: { + reasoning: false, + temperature: 0.8, + topP: 0.9, + topK: 40, + }, + }; + + const chatOptions = { + context: session.context, + }; + + let keepProcessing = true; + do { + const response = await AiService.chat(workOrder.provider, modelConfig, chatOptions); + keepProcessing = (response.tool_calls?.length ?? 0) > 0; + for (const tool_call of response.tool_calls ?? []) { + const result = await aiCallTool(tool_call.function.name, tool_call.function.arguments); + messages.push({ role: "tool", content: result }); + /* emit turn-tool-call socket message */ + } + } while (keepProcessing); + + /* emit turn-finished socket message */ + } + + buildSessionContext(session: IChatSession, messages: IChatMessage[]): void { + let content; + for (const message of session.context) { + switch (message.role) { + case "system": + continue; + case "user": + content = message.content; + break; + case "assistant": + content = message.content; + break; + case "tool": + content = message.content; + break; + } + messages.push({ role: message.role, content: message.content }); + } + } + + /** + * To optimize context, reduce clutter, and help the agent focus, full outputs + * of older file reads and edits are summarized once a newer version is available. + */ + pruneSessionContext(messages: IContextChatMessage[]): void { + // TODO + } +} + +export default new AgentService(); diff --git a/gadget-drone/src/services/ai.ts b/gadget-drone/src/services/ai.ts new file mode 100644 index 0000000..3063932 --- /dev/null +++ b/gadget-drone/src/services/ai.ts @@ -0,0 +1,95 @@ +// src/services/ai.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import { GadgetService } from "../lib/service.ts"; +import { + type IAiChatOptions, + type IAiChatResponse, + type IAiGenerateOptions, + type IAiGenerateResponse, + type IAiModelConfig, + type IAiProvider, + type IAiResponseStreamFn, + createAiApi, +} from "@gadget/ai"; + +/** + * An abstraction of the backend AI APIs (Ollama, OpenAI) that provides one + * common interface and contract for working with different AI APIs. + * + * We are Ollama-first and prefer the Ollama API because it offers more actual + * control over the AI models, their performance, and their results at runtime. + * + * OpenAI, however, is the most ubiquitous API, and expands the range of service + * providers developers can use with the Gadget ecosystem. + */ +class AiService extends GadgetService { + get name(): string { + return "AiService"; + } + get slug(): string { + return "svc:ai"; + } + + async start(): Promise { + this.log.info("started"); + } + + async stop(): Promise { + this.log.info("stopped"); + } + + /** + * Query the list of models available from the provider, then queries the + * models for their individual capabilities. The results are cached in the Gadget + */ + async discovery(provider: IAiProvider): Promise { + this.log.info("discovering provider model list", { + name: provider.name, + sdk: provider.sdk, + }); + const api = this.getApi(provider); + const response = await api.listModels(); + this.log.debug("listModels response", { response }); + } + + async generate( + provider: IAiProvider, + model: IAiModelConfig, + options: IAiGenerateOptions, + streamCallback?: IAiResponseStreamFn, + ): Promise { + this.log.info("calling provider to generate a response", { + name: provider.name, + sdk: provider.sdk, + haveStreamCallback: !!streamCallback, + options, + }); + const api = this.getApi(provider); + return api.generate(model, options, streamCallback); + } + + async chat( + provider: IAiProvider, + model: IAiModelConfig, + options: IAiChatOptions, + streamCallback?: IAiResponseStreamFn, + ): Promise { + this.log.info("calling provider to process chat", { + provider: provider.name, + sdk: provider.sdk, + haveStreamCallback: !!streamCallback, + model, + options, + }); + const api = this.getApi(provider); + return await api.chat(model, options, streamCallback); + } + + getApi(provider: IAiProvider) { + return createAiApi(provider, this.log); + } +} + +export default new AiService(); diff --git a/gadget-drone/src/services/platform.ts b/gadget-drone/src/services/platform.ts new file mode 100644 index 0000000..acaadd7 --- /dev/null +++ b/gadget-drone/src/services/platform.ts @@ -0,0 +1,168 @@ +// src/services/platform.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import env from "../config/env.ts"; + +import assert from "node:assert"; +import path from "node:path"; +import os from "node:os"; + +import { GadgetService } from "../lib/service.ts"; + +export enum DroneStatus { + Starting = "starting", + Available = "available", + Busy = "busy", + Offline = "offline", +} + +export interface PlatformRegistration { + _id: string; // your drone's registration ID, channel, and queue + user: { + _id: string; + username: string; + }; +} + +interface PlatformApiResponse { + success: boolean; + message?: string; +} + +interface PlatformRegistrationResponse extends PlatformApiResponse { + data: PlatformRegistration; +} + +class PlatformService extends GadgetService { + registration: PlatformRegistration | undefined; + + get name(): string { + return "PlatformService"; + } + get slug(): string { + return "svc:platform"; + } + + async start(): Promise { + this.log.info("started"); + } + + async stop(): Promise { + this.log.info("stopped"); + } + + async register( + email: string, + password: string, + ): Promise { + const url = this.getApiUrl("/drone/registration"); + const body = JSON.stringify({ + email, + password, + hostname: os.hostname(), + }); + const response = await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "Content-Length": body.length.toString(), + "X-Gadget-Key": env.platform.apiKey, + }, + body, + }); + + const json = (await response.json()) as PlatformRegistrationResponse; + if (!json.success) { + const error = new Error("failed to register drone with Platform"); + error.name = "PlatformError"; + error.statusCode = response.status; + throw error; + } + if (!json.data || !json.data._id) { + const error = new Error( + "registration response did not contain required data", + ); + error.name = "PlatformError"; + throw error; + } + + if (!json.data.user || !json.data.user._id) { + const error = new Error( + "registration response did not contain required user account data", + ); + error.name = "PlatformError"; + throw error; + } + + this.registration = json.data; + + return json.data; + } + + async unregister(): Promise { + if (!this.registration) { + return; + } + + const url = this.getApiUrl(`/drone/registration/${this.registration._id}`); + const body = JSON.stringify({ registration: this.registration }); + const response = await fetch(url, { + method: "DELETE", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "Content-Length": body.length.toString(), + "X-Gadget-Key": env.platform.apiKey, + }, + body, + }); + + const json = (await response.json()) as PlatformRegistrationResponse; + if (!json.success) { + const error = new Error("failed to register drone with Platform"); + error.name = "PlatformError"; + error.statusCode = response.status; + throw error; + } + } + + async setStatus(status: DroneStatus): Promise { + assert( + this.registration, + "must register with platform before setting status", + ); + + const url = this.getApiUrl( + `/drone/registration/${this.registration._id}/status`, + ); + const body = JSON.stringify({ status }); + const response = await fetch(url, { + method: "PUT", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "Content-Length": body.length.toString(), + "X-Gadget-Key": env.platform.apiKey, + }, + body, + }); + + const json = (await response.json()) as PlatformRegistrationResponse; + if (!json.success) { + const error = new Error("failed to register drone with Platform"); + error.name = "PlatformError"; + error.statusCode = response.status; + throw error; + } + + this.log.info("drone status updated on platform", { status }); + } + + getApiUrl(url: string): string { + return `${env.platform.baseUrl}/api/v1${url}`; + } +} + +export default new PlatformService(); diff --git a/gadget-drone/tsconfig.json b/gadget-drone/tsconfig.json new file mode 100644 index 0000000..f86d33b --- /dev/null +++ b/gadget-drone/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "typeRoots": ["node_modules/@types", "types"], + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "files": ["types/error.d.ts"] +} diff --git a/gadget-drone/types/error.d.ts b/gadget-drone/types/error.d.ts new file mode 100644 index 0000000..b681acf --- /dev/null +++ b/gadget-drone/types/error.d.ts @@ -0,0 +1,7 @@ +// types/error.d.ts +// Copyright (C) 2025 DTP Technologies, LLC +// All Rights Reserved + +interface Error { + statusCode?: number; +} diff --git a/gadget.code-workspace b/gadget.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/gadget.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..84c463f --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "gadget-workspace", + "version": "1.0.0", + "private": true, + "description": "Gadget Code monorepo workspace root", + "packageManager": "pnpm@10.33.2", + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/packages/ai/.gitignore b/packages/ai/.gitignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/packages/ai/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/packages/ai/README.md b/packages/ai/README.md new file mode 100644 index 0000000..ddc0670 --- /dev/null +++ b/packages/ai/README.md @@ -0,0 +1,98 @@ +# @gadget/ai + +Gadget Code's AI API abstraction layer. Provides a single internal API contract for calling AI providers (Ollama, OpenAI) without consumer code knowing which provider is configured. + +## Principles + +1. **One interface, all providers.** Consumer code calls `createAiApi()` once and holds the resulting `AiApi`. It never checks `provider.sdk` again. +2. **All AI SDK knowledge is contained here.** No consumer imports `ollama` or `openai` SDKs directly. +3. **Responses are normalized.** All provider responses are translated to Gadget Code's internal interface types before returning. + +## Usage + +```typescript +import { createAiApi } from "@gadget/ai"; + +const provider = { + _id: "local-ollama", + name: "Local Ollama", + sdk: "ollama", + baseUrl: "http://localhost:11434", + apiKey: "", + defaultModelId: "llama3.2", +}; + +const modelConfig = { + provider, + modelId: "llama3.2", + params: { + reasoning: false, + temperature: 0.8, + topP: 0.9, + topK: 40, + }, +}; + +const ai = createAiApi(provider, logger); + +const result = await ai.generate(modelConfig, { + prompt: "Explain what this code does", + systemPrompt: "You are a code reviewer.", +}); +console.log(result.response); +console.log(result.stats.duration.text); // formatted, e.g. "00:00:02" +``` + +## API + +### Factory + +**`createAiApi(provider, logger?)`** — Returns an `AiApi` instance for the given provider. `logger` is optional and defaults to a no-op logger. Pass your own logger to receive debug output. + +### AiApi + +Abstract base class. Currently implemented: + +- **`AiOllamaApi`** — Ollama provider +- **`AiOpenAiApi`** — OpenAI provider (stubbed) + +#### `ai.generate(model, options, streamCallback?)` + +Single-prompt generation. Returns `IAiGenerateResponse`. + +#### `ai.chat(model, options, streamCallback?)` + +Chat with conversation history. Pass `options.context` for multi-turn对话. Returns `IAiChatResponse`. + +### Interfaces + +All interfaces are exported for use by consumers: + +- **`IAiProvider`** — AI provider configuration +- **`IAiModelConfig`** — Model + runtime parameters +- **`IAiGenerateOptions`** / **`IAiGenerateResponse`** +- **`IAiChatOptions`** / **`IAiChatResponse`** — includes `tool_calls` for function-calling models +- **`IAiInferenceStats`** — token counts and duration (both raw `seconds` number and formatted `text` string) +- **`IAiLogger`** — injectable logger interface (`debug`, `info`, `warn`, `error`) + +## Providers + +### Ollama + +Configured via `IAiProvider` with `sdk: "ollama"`. Uses the `ollama` npm package. Handles streaming responses and normalizes Ollama-specific response fields (thinking tokens, token counts, duration). + +### OpenAI + +Configured via `IAiProvider` with `sdk: "openai"`. Stubbed — `chat()` and `generate()` throw `"Not yet implemented"`. Implement by wiring the `openai` npm package following the same pattern as `AiOllamaApi`. + +## Duration Formatting + +The library uses `numeral` to provide a consistent formatted duration string (`stats.duration.text`) in `hh:mm:ss` format. The raw nanosecond value is also returned in `stats.duration.seconds` for consumers that need the raw number. + +## Adding a New Provider + +1. Create `packages/ai/src/.ts` — extend `AiApi`, implement all abstract methods +2. Update `packages/ai/src/index.ts` — add the new class to the `createAiApi` factory switch +3. Update this README + +No consumer code changes required. \ No newline at end of file diff --git a/packages/ai/package.json b/packages/ai/package.json new file mode 100644 index 0000000..a290381 --- /dev/null +++ b/packages/ai/package.json @@ -0,0 +1,31 @@ +{ + "name": "@gadget/ai", + "version": "1.0.0", + "description": "Gadget Code AI API abstraction layer", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "keywords": ["gadget", "ai", "ollama", "openai", "abstraction"], + "author": "Rob Colbert", + "license": "Apache-2.0", + "dependencies": { + "numeral": "^2.0.6", + "ollama": "^0.6.3", + "openai": "^6.34.0" + }, + "devDependencies": { + "@types/node": "^25.6.0", + "@types/numeral": "^2.0.5", + "typescript": "^6.0.3" + } +} \ No newline at end of file diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts new file mode 100644 index 0000000..264d3f2 --- /dev/null +++ b/packages/ai/src/api.ts @@ -0,0 +1,132 @@ +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +export type AiSdkType = "ollama" | "openai"; + +export interface IAiProvider { + _id: string; + name: string; + sdk: AiSdkType; + baseUrl: string; + apiKey: string; + defaultModelId?: string; +} + +export interface IAiModelConfig { + provider: IAiProvider; + modelId: string; + params: { + reasoning: boolean | "high" | "medium" | "low"; + temperature: number; + topP: number; + topK: number; + }; +} + +export interface IAiInferenceStats { + tokenCounts: { + input: number; + thinking: number; + response: number; + }; + duration: { + seconds: number; + text: string; + }; +} + +export interface IAiGenerateOptions { + prompt: string; + systemPrompt?: string; +} + +export interface IAiGenerateResponse { + response: string; + thinking?: string; + stats: IAiInferenceStats; + done: boolean; + doneReason?: string; +} + +export interface IContextChatMessage { + createdAt: Date; + role: string; + content: string; + user?: { + _id: string; + username: string; + displayName: string; + }; +} + +export interface IAiChatOptions { + systemPrompt?: string; + userPrompt?: string; + context?: IContextChatMessage[]; +} + +export interface IToolCall { + call_id: string; + function: { + name: string; + arguments: string; + }; +} + +export interface IAiChatResponse { + response: string; + thinking?: string; + stats: IAiInferenceStats; + done: boolean; + doneReason?: string; + tool_calls?: IToolCall[]; +} + +export interface IAiStreamChunk { + data: string; +} + +export type IAiResponseStreamFn = (chunk: IAiStreamChunk) => Promise; + +export interface IAiLogger { + debug(message: string, metadata?: unknown): Promise | void; + info(message: string, metadata?: unknown): Promise | void; + warn(message: string, metadata?: unknown): Promise | void; + error(message: string, metadata?: unknown): Promise | void; +} + +const noOpLogger: IAiLogger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +}; + +export function defaultLogger(): IAiLogger { + return noOpLogger; +} + +export abstract class AiApi { + protected provider: IAiProvider; + protected log: IAiLogger; + + constructor(provider: IAiProvider, logger?: IAiLogger) { + this.provider = provider; + this.log = logger ?? defaultLogger(); + } + + abstract listModels(): Promise; + abstract probeModel(modelId: string): Promise; + + abstract generate( + model: IAiModelConfig, + options: IAiGenerateOptions, + streamCallback?: IAiResponseStreamFn, + ): Promise; + + abstract chat( + model: IAiModelConfig, + options: IAiChatOptions, + streamCallback?: IAiResponseStreamFn, + ): Promise; +} \ No newline at end of file diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts new file mode 100644 index 0000000..59d9250 --- /dev/null +++ b/packages/ai/src/index.ts @@ -0,0 +1,40 @@ +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +export { + type AiSdkType, + type IAiProvider, + type IAiModelConfig, + type IAiInferenceStats, + type IAiGenerateOptions, + type IAiGenerateResponse, + type IContextChatMessage, + type IAiChatOptions, + type IAiChatResponse, + type IToolCall, + type IAiStreamChunk, + type IAiResponseStreamFn, + type IAiLogger, + defaultLogger, + AiApi, +} from "./api.js"; + +export { AiOllamaApi } from "./ollama.js"; +export { AiOpenAiApi } from "./openai.js"; + +import { AiOllamaApi } from "./ollama.js"; +import { AiOpenAiApi } from "./openai.js"; +import type { IAiProvider } from "./api.js"; +import type { IAiLogger } from "./api.js"; +import type { AiApi } from "./api.js"; + +export function createAiApi(provider: IAiProvider, logger?: IAiLogger): AiApi { + switch (provider.sdk) { + case "ollama": + return new AiOllamaApi(provider, logger); + case "openai": + return new AiOpenAiApi(provider, logger); + default: + throw new Error(`Unknown AI SDK: ${(provider as IAiProvider).sdk}`); + } +} \ No newline at end of file diff --git a/packages/ai/src/ollama.ts b/packages/ai/src/ollama.ts new file mode 100644 index 0000000..9b484b9 --- /dev/null +++ b/packages/ai/src/ollama.ts @@ -0,0 +1,125 @@ +// src/ollama.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import assert from "node:assert"; + +import { Ollama } from "ollama"; +import numeral from "numeral"; +import { + AiApi, + IAiChatOptions, + IAiChatResponse, + IAiGenerateOptions, + IAiGenerateResponse, + IAiLogger, + IAiModelConfig, + IAiProvider, + IAiResponseStreamFn, +} from "./api.js"; + +export class AiOllamaApi extends AiApi { + protected client: Ollama; + + constructor(provider: IAiProvider, logger?: IAiLogger) { + super(provider, logger); + this.client = new Ollama({ + host: this.provider.baseUrl, + headers: { Authorization: `Bearer ${this.provider.apiKey}` }, + }); + } + + async listModels(): Promise { + await this.log.debug("AiOllamaApi.listModels called"); + } + + async probeModel(modelId: string): Promise { + await this.log.debug("AiOllamaApi.probeModel called", { modelId }); + } + + async generate( + model: IAiModelConfig, + options: IAiGenerateOptions, + streamCallback?: IAiResponseStreamFn, + ): Promise { + await this.log.debug("AiOllamaApi.generate called", { + provider: model.provider.name, + modelId: model.modelId, + }); + + const response = await this.client.generate({ + model: model.modelId, + prompt: options.prompt, + system: options.systemPrompt, + stream: true, + }); + + let lastChunk; + for await (const chunk of response) { + await this.log.debug("stream chunk received", { chunk }); + lastChunk = chunk; + } + assert(lastChunk, "no stream response chunks received"); + + return { + done: lastChunk.done, + doneReason: lastChunk.done_reason, + response: lastChunk.response, + thinking: lastChunk.thinking, + stats: { + duration: { + seconds: lastChunk.total_duration, + text: numeral(lastChunk.total_duration).format("hh:mm:ss"), + }, + tokenCounts: { + input: lastChunk.prompt_eval_count, + response: lastChunk.eval_count, + thinking: 0, + }, + }, + }; + } + + async chat( + model: IAiModelConfig, + options: IAiChatOptions, + streamCallback?: IAiResponseStreamFn, + ): Promise { + await this.log.debug("AiOllamaApi.chat called", { + provider: model.provider.name, + modelId: model.modelId, + }); + + const response = await this.client.chat({ + model: model.modelId, + messages: options.context, + stream: true, + think: model.params.reasoning, + }); + + let lastChunk; + for await (const chunk of response) { + await this.log.debug("stream chunk received", { chunk }); + lastChunk = chunk; + } + assert(lastChunk, "no response chunks received"); + + return { + done: lastChunk.done, + doneReason: lastChunk.done_reason, + response: lastChunk.message.content, + thinking: lastChunk.message.thinking, + stats: { + duration: { + seconds: lastChunk.total_duration, + text: numeral(lastChunk.total_duration).format("hh:mm:ss"), + }, + tokenCounts: { + input: lastChunk.prompt_eval_count, + response: lastChunk.eval_count, + thinking: 0, + }, + }, + }; + } +} diff --git a/packages/ai/src/openai.ts b/packages/ai/src/openai.ts new file mode 100644 index 0000000..d0fa633 --- /dev/null +++ b/packages/ai/src/openai.ts @@ -0,0 +1,45 @@ +// src/openai.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import { + AiApi, + IAiChatOptions, + IAiChatResponse, + IAiGenerateOptions, + IAiGenerateResponse, + IAiLogger, + IAiModelConfig, + IAiProvider, + IAiResponseStreamFn, +} from "./api.js"; + +export class AiOpenAiApi extends AiApi { + constructor(provider: IAiProvider, logger?: IAiLogger) { + super(provider, logger); + } + + async listModels(): Promise { + await this.log.debug("AiOpenAiApi.listModels called"); + } + + async probeModel(modelId: string): Promise { + await this.log.debug("AiOpenAiApi.probeModel called", { modelId }); + } + + async generate( + _model: IAiModelConfig, + _options: IAiGenerateOptions, + _streamCallback?: IAiResponseStreamFn, + ): Promise { + throw new Error("Not yet implemented"); + } + + async chat( + _model: IAiModelConfig, + _options: IAiChatOptions, + _streamCallback?: IAiResponseStreamFn, + ): Promise { + throw new Error("Not yet implemented"); + } +} diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json new file mode 100644 index 0000000..6414465 --- /dev/null +++ b/packages/ai/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "typeRoots": ["node_modules/@types", "types"], + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} \ No newline at end of file diff --git a/packages/ai/types/error.d.ts b/packages/ai/types/error.d.ts new file mode 100644 index 0000000..b681acf --- /dev/null +++ b/packages/ai/types/error.d.ts @@ -0,0 +1,7 @@ +// types/error.d.ts +// Copyright (C) 2025 DTP Technologies, LLC +// All Rights Reserved + +interface Error { + statusCode?: number; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f3ff179 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5951 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + gadget-code: + dependencies: + '@fortawesome/fontawesome-free': + specifier: ^6.7.2 + version: 6.7.2 + '@gadget/ai': + specifier: workspace:* + version: link:../packages/ai + ansicolor: + specifier: ^2.0.3 + version: 2.0.3 + bull: + specifier: ^4.16.5 + version: 4.16.5 + chart.js: + specifier: ^4.5.0 + version: 4.5.1 + compression: + specifier: ^1.8.0 + version: 1.8.1 + connect-redis: + specifier: ^8.0.2 + version: 8.1.0(express-session@1.19.0) + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + cron: + specifier: ^4.3.1 + version: 4.4.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.20 + dotenv: + specifier: ^16.6.0 + version: 16.6.1 + dtp-cleantext: + specifier: ^1.0.0 + version: 1.0.0 + express: + specifier: ^5.1.0 + version: 5.2.1 + express-rate-limit: + specifier: ^7.5.1 + version: 7.5.1(express@5.2.1) + express-session: + specifier: ^1.18.1 + version: 1.19.0 + geoip-lite: + specifier: ^1.4.10 + version: 1.4.10 + has-flag: + specifier: ^5.0.1 + version: 5.0.1 + ioredis: + specifier: ^5.6.0 + version: 5.10.1 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 + marked: + specifier: ^16.0.0 + version: 16.4.2 + method-override: + specifier: ^3.0.0 + version: 3.0.0 + minio: + specifier: ^8.0.5 + version: 8.0.7 + mongoose: + specifier: ^8.16.1 + version: 8.23.1 + morgan: + specifier: ^1.10.0 + version: 1.10.1 + multer: + specifier: ^2.0.1 + version: 2.1.1 + nodemailer: + specifier: ^7.0.3 + version: 7.0.13 + numeral: + specifier: ^2.0.6 + version: 2.0.6 + pug: + specifier: ^3.0.3 + version: 3.0.4 + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + react-router-dom: + specifier: ^7.14.2 + version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + rotating-file-stream: + specifier: ^3.2.6 + version: 3.2.9 + serve-favicon: + specifier: ^2.5.1 + version: 2.5.1 + socket.io: + specifier: ^4.8.3 + version: 4.8.3 + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + uikit: + specifier: ^3.23.11 + version: 3.25.16 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + devDependencies: + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 + '@tailwindcss/postcss': + specifier: ^4.2.4 + version: 4.2.4 + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/browser-sync': + specifier: ^2.29.0 + version: 2.29.1 + '@types/compression': + specifier: ^1.8.1 + version: 1.8.1 + '@types/cookie-parser': + specifier: ^1.4.9 + version: 1.4.10(@types/express@5.0.6) + '@types/express': + specifier: ^5.0.3 + version: 5.0.6 + '@types/express-session': + specifier: ^1.18.2 + version: 1.19.0 + '@types/geoip-lite': + specifier: ^1.4.4 + version: 1.4.4 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/less': + specifier: ^3.0.8 + version: 3.0.8 + '@types/method-override': + specifier: ^3.0.0 + version: 3.0.0(@types/express@5.0.6) + '@types/morgan': + specifier: ^1.9.10 + version: 1.9.10 + '@types/multer': + specifier: ^1.4.13 + version: 1.4.13 + '@types/node': + specifier: ^24.0.4 + version: 24.12.2 + '@types/nodemailer': + specifier: ^6.4.17 + version: 6.4.23 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/serve-favicon': + specifier: ^2.5.7 + version: 2.5.7 + '@types/uikit': + specifier: ^3.14.5 + version: 3.23.0 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0)) + autoprefixer: + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.12) + browser-sync: + specifier: ^3.0.4 + version: 3.0.4 + esbuild: + specifier: ^0.25.5 + version: 0.25.12 + globals: + specifier: ^16.2.0 + version: 16.5.0 + jsdom: + specifier: ^29.0.2 + version: 29.1.0 + less: + specifier: ^4.3.0 + version: 4.6.4 + postcss: + specifier: ^8.5.10 + version: 8.5.12 + tailwindcss: + specifier: ^4.2.4 + version: 4.2.4 + tsc-alias: + specifier: ^1.0.7 + version: 1.8.16 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + tsx: + specifier: ^4.19.2 + version: 4.21.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^8.0.10 + version: 8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0) + vitest: + specifier: ^4.1.5 + version: 4.1.5(@types/node@24.12.2)(jsdom@29.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0)) + + gadget-drone: + dependencies: + '@gadget/ai': + specifier: workspace:* + version: link:../packages/ai + ansicolor: + specifier: ^2.0.3 + version: 2.0.3 + dayjs: + specifier: ^1.11.20 + version: 1.11.20 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + numeral: + specifier: ^2.0.6 + version: 2.0.6 + ollama: + specifier: ^0.6.3 + version: 0.6.3 + openai: + specifier: ^6.34.0 + version: 6.34.0(ws@8.18.3) + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + devDependencies: + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 + prettier: + specifier: ^3.8.3 + version: 3.8.3 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + + packages/ai: + dependencies: + numeral: + specifier: ^2.0.6 + version: 2.0.6 + ollama: + specifier: ^0.6.3 + version: 0.6.3 + openai: + specifier: ^6.34.0 + version: 6.34.0(ws@8.18.3) + devDependencies: + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@fortawesome/fontawesome-free@6.7.2': + resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==} + engines: {node: '>=6'} + + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@mongodb-js/saslprep@1.4.9': + resolution: {integrity: sha512-RXSxsokhAF/4nWys8An8npsqOI33Ex1Hlzqjw2pZOO+GKtMAR2noGnUdsFiGwsaO/xXI+56mtjTmDA3JXJsvmA==} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tailwindcss/node@4.2.4': + resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + + '@tailwindcss/oxide-android-arm64@4.2.4': + resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.4': + resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.4': + resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.4': + resolution: {integrity: sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/browser-sync@2.29.1': + resolution: {integrity: sha512-jAMsEkLpNURfpS4XIN9BX7SY+uCoTkPjLIovwssV/3e/FPwg9hYusbCXmGNfC3T6W/6d3iP3clxy9cvevjFKtQ==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/compression@1.8.1': + resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} + peerDependencies: + '@types/express': '*' + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express-session@1.19.0': + resolution: {integrity: sha512-GbypG0bog68UbOq2tSAp7SclvCUm3ha1uDi58OPRGK1NfRvCIu7Gz0M7fTGtpNG1T9a29GpuurQj9zEcT/lMXQ==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/geoip-lite@1.4.4': + resolution: {integrity: sha512-2uVfn+C6bX/H356H6mjxsWUA5u8LO8dJgSBIRO/NFlpMe4DESzacutD/rKYrTDKm1Ugv78b4Wz1KvpHrlv3jSw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/less@3.0.8': + resolution: {integrity: sha512-Gjm4+H9noDJgu5EdT3rUw5MhPBag46fiOy27BefvWkNL8mlZnKnCaVVVTLKj6RYXed9b62CPKnPav9govyQDzA==} + + '@types/luxon@3.7.1': + resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} + + '@types/method-override@3.0.0': + resolution: {integrity: sha512-7XFHR6j7JljprBpzzRZatakUXm1kEGAM3PL/GSsGRHtDvOAKYCdmnXX/5YSl1eQrpJymGs9tRekSWEGaG+Ntjw==} + peerDependencies: + '@types/express': '*' + + '@types/micromatch@2.3.35': + resolution: {integrity: sha512-J749bHo/Zu56w0G0NI/IGHLQPiSsjx//0zJhfEVAN95K/xM5C8ZDmhkXtU3qns0sBOao7HuQzr8XV1/2o5LbXA==} + + '@types/morgan@1.9.10': + resolution: {integrity: sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/multer@1.4.13': + resolution: {integrity: sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/nodemailer@6.4.23': + resolution: {integrity: sha512-aFV3/NsYFLSx9mbb5gtirBSXJnAlrusoKNuPbxsASWc7vrKLmIrTQRpdcxNcSFL3VW2A2XpeLEavwb2qMi6nlQ==} + + '@types/numeral@2.0.5': + resolution: {integrity: sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==} + + '@types/parse-glob@3.0.32': + resolution: {integrity: sha512-n4xmml2WKR12XeQprN8L/sfiVPa8FHS3k+fxp4kSr/PA2GsGUgFND+bvISJxM0y5QdvzNEGjEVU3eIrcKks/pA==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-favicon@2.5.7': + resolution: {integrity: sha512-z9TNUQXdQ+W/OJMP1e3KOYUZ99qJS4+ZfFOIrPGImcayqKoyifbJSEFkVq1MCKBbqjMZpjPj3B5ilrQAR2+TOw==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/uikit@3.23.0': + resolution: {integrity: sha512-GTn8/K+f4AjFxtLqRKWzjaVKckQp/rHcl20IO09salh2VjyFb9CqeFugL95skO6qbbJLil3PE1MmNajrSj5gMg==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansicolor@2.0.3: + resolution: {integrity: sha512-pzusTqk9VHrjgMCcTPDTTvfJfx6Q3+L5tQ6yKC8Diexmoit4YROTFIkxFvRTNL9y5s0Q8HrSrgerCD5bIC+Kiw==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-each-series@0.1.1: + resolution: {integrity: sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==} + engines: {node: '>=0.8.0'} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + babel-walk@3.0.0-canary-5: + resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} + engines: {node: '>= 10.0.0'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + baseline-browser-mapping@2.10.23: + resolution: {integrity: sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==} + engines: {node: '>=6.0.0'} + hasBin: true + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + + browser-sync-client@3.0.4: + resolution: {integrity: sha512-+ew5ubXzGRKVjquBL3u6najS40TG7GxCdyBll0qSRc/n+JRV9gb/yDdRL1IAgRHqjnJTdqeBKKIQabjvjRSYRQ==} + engines: {node: '>=8.0.0'} + + browser-sync-ui@3.0.4: + resolution: {integrity: sha512-5Po3YARCZ/8yQHFzvrSjn8+hBUF7ZWac39SHsy8Tls+7tE62iq6pYWxpVU6aOOMAGD21RwFQhQeqmJPf70kHEQ==} + + browser-sync@3.0.4: + resolution: {integrity: sha512-mcYOIy4BW6sWSEnTSBjQwWsnbx2btZX78ajTTjdNfyC/EqQVcIe0nQR6894RNAMtvlfAnLaH9L2ka97zpvgenA==} + engines: {node: '>= 8.0.0'} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-recipes@1.3.4: + resolution: {integrity: sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==} + + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bull@4.16.5: + resolution: {integrity: sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==} + engines: {node: '>=12'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001791: + resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-parser@2.2.0: + resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + connect-history-api-fallback@1.6.0: + resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==} + engines: {node: '>=0.8'} + + connect-redis@8.1.0: + resolution: {integrity: sha512-Km0EYLDlmExF52UCss5gLGTtrukGC57G6WCC2aqEMft5Vr4xNWuM4tL+T97kWrw+vp40SXFteb6Xk/7MxgpwdA==} + engines: {node: '>=18'} + peerDependencies: + express-session: '>=1' + + connect@3.6.6: + resolution: {integrity: sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==} + engines: {node: '>= 0.10.0'} + + constantinople@4.0.1: + resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + + cron@4.4.0: + resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} + engines: {node: '>=18.x'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dev-ip@1.0.1: + resolution: {integrity: sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==} + engines: {node: '>= 0.8.0'} + hasBin: true + + diacritics@1.3.0: + resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctypes@1.1.0: + resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + drange@1.1.1: + resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} + engines: {node: '>=4'} + + dtp-cleantext@1.0.0: + resolution: {integrity: sha512-V6+fTe/pv+w0+/z/P1ypjezUi9bBmuryuubAtXHknEG+PKLuWfPMsW/Xs7wyD3ZYUR7mLAXM4DXt1AiZ+PTKMw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + easy-extender@2.3.4: + resolution: {integrity: sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==} + engines: {node: '>= 4.0.0'} + + eazy-logger@4.1.0: + resolution: {integrity: sha512-+mn7lRm+Zf1UT/YaH8WXtpU6PIV2iOjzP6jgKoiaq/VNrjYKp+OHZGe2znaLgDeFkw8cL9ffuaUm+nNnzcYyGw==} + engines: {node: '>= 0.8.0'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.344: + resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.7: + resolution: {integrity: sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.21.0: + resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} + engines: {node: '>=10.13.0'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express-session@1.19.0: + resolution: {integrity: sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==} + engines: {node: '>= 0.8.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-xml-builder@1.1.5: + resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} + + fast-xml-parser@5.7.2: + resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + finalhandler@1.1.0: + resolution: {integrity: sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@3.0.1: + resolution: {integrity: sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + geoip-lite@1.4.10: + resolution: {integrity: sha512-4N69uhpS3KFd97m00wiFEefwa+L+HT5xZbzPhwu+sDawStg6UN/dPwWtUfkQuZkGIY1Cj7wDVp80IsqNtGMi2w==} + engines: {node: '>=10.3.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-flag@5.0.1: + resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} + engines: {node: '>=12'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + immutable@3.8.3: + resolution: {integrity: sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==} + engines: {node: '>=0.10.0'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} + + ip-address@5.9.4: + resolution: {integrity: sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==} + engines: {node: '>= 0.10'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-expression@4.0.0: + resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number-like@1.0.8: + resolution: {integrity: sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@1.1.0: + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} + + isnumber@1.0.0: + resolution: {integrity: sha512-JLiSz/zsZcGFXPrB4I/AGBvtStkt+8QmksyZBZnVXnnK9XdTEyz0tX8CRYljtwYDuIuZzih6DpHQdi+3Q6zHPw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-stringify@1.0.2: + resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jsdom@29.1.0: + resolution: {integrity: sha512-YNUc7fB9QuvSSQWfrH0xF+TyABkxUwx8sswgIDaCrw4Hol8BghdZDkITtZheRJeMtzWlnTfsM3bBBusRvpO1wg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsonfile@3.0.1: + resolution: {integrity: sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jstransformer@1.0.0: + resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + kareem@2.6.3: + resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} + engines: {node: '>=12.0.0'} + + lazy@1.0.11: + resolution: {integrity: sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==} + engines: {node: '>=0.2.0'} + + less@4.6.4: + resolution: {integrity: sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==} + engines: {node: '>=18'} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isfinite@3.3.2: + resolution: {integrity: sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + method-override@3.0.0: + resolution: {integrity: sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==} + engines: {node: '>= 0.10'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minio@8.0.7: + resolution: {integrity: sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==} + engines: {node: ^16 || ^18 || >=20} + + mitt@1.2.0: + resolution: {integrity: sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==} + + mongodb-connection-string-url@3.0.2: + resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + + mongodb@6.20.0: + resolution: {integrity: sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.3.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + + mongoose@8.23.1: + resolution: {integrity: sha512-gHSPD8qEwRmiXapK17hEnFWZdcFENMegHTcw5XIIg2+7R8eXQvdwSiMpD/A2oG8tKzFLLHyRXd8/eaDPAVwZgQ==} + engines: {node: '>=16.20.1'} + + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + + mpath@0.9.0: + resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} + engines: {node: '>=4.0.0'} + + mquery@5.0.0: + resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} + engines: {node: '>=14.0.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.10: + resolution: {integrity: sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==} + + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + + mylas@2.1.14: + resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} + engines: {node: '>=16.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + needle@3.5.0: + resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} + engines: {node: '>= 4.4.x'} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + nodemailer@7.0.13: + resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==} + engines: {node: '>=6.0.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + numeral@2.0.6: + resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ollama@0.6.3: + resolution: {integrity: sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openai@6.34.0: + resolution: {integrity: sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + opn@5.3.0: + resolution: {integrity: sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==} + engines: {node: '>=4'} + + parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + portscanner@2.2.0: + resolution: {integrity: sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==} + engines: {node: '>=0.4', npm: '>=1.0.0'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + engines: {node: ^10 || ^12 || >=14} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + pug-attrs@3.0.0: + resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} + + pug-code-gen@3.0.4: + resolution: {integrity: sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==} + + pug-error@2.1.0: + resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} + + pug-filters@4.0.0: + resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} + + pug-lexer@5.0.1: + resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} + + pug-linker@4.0.0: + resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} + + pug-load@3.0.0: + resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} + + pug-parser@6.0.0: + resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} + + pug-runtime@3.0.1: + resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} + + pug-strip-comments@2.0.0: + resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} + + pug-walk@2.0.0: + resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} + + pug@3.0.4: + resolution: {integrity: sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + randexp@0.5.3: + resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==} + engines: {node: '>=4'} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-router-dom@7.14.2: + resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.14.2: + resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + resp-modifier@6.0.2: + resolution: {integrity: sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==} + engines: {node: '>= 0.8.0'} + + ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rotating-file-stream@3.2.9: + resolution: {integrity: sha512-i9i0KkHh12ryl4xtELg+0gyoFre2PJ9RcQQLzquWsiqygyYsrZLckrqqYrthhnJZGZb4g+KUHtcoWYVq34gaug==} + engines: {node: '>=14.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rx@4.1.0: + resolution: {integrity: sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-favicon@2.5.1: + resolution: {integrity: sha512-JndLBslCLA/ebr7rS3d+/EKkzTsTi1jI2T9l+vHfAaGJ7A7NhtDpSZ0lx81HCNWnnE0yHncG+SSnVf9IMxOwXQ==} + engines: {node: '>= 0.8.0'} + + serve-index@1.9.2: + resolution: {integrity: sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + server-destroy@1.0.1: + resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shoetest@1.2.2: + resolution: {integrity: sha512-iT8kIEFcGfUwo53VUFckm+glTkc0oLycRe+YqU/W4wQuIHGIWc5KMIpDnJVdavKCyEZKQTi8IDq27rDmB09QjA==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + sift@17.1.3: + resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + socket.io-adapter@2.5.6: + resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} + + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.6: + resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + sprintf-js@1.1.2: + resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + stats-lite@2.2.0: + resolution: {integrity: sha512-/Kz55rgUIv2KP2MKphwYT/NCuSfAlbbMRv2ZWw7wyXayu230zdtzhxxuXXcvsc6EmmhS8bSJl3uS1wmMHFumbA==} + engines: {node: '>=2.0.0'} + + statuses@1.3.1: + resolution: {integrity: sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==} + engines: {node: '>= 0.6'} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + stream-throttle@0.1.3: + resolution: {integrity: sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==} + engines: {node: '>= 0.10.0'} + hasBin: true + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + striptags@3.2.0: + resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} + + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwindcss@4.2.4: + resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-stream@1.0.0: + resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} + hasBin: true + + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + + uikit@3.25.16: + resolution: {integrity: sha512-zTpbaNMWTTsWZOiFJkFHa+zia1pRgY3FA6hEAckVgk4aJ19+qie7M/qH9GJgI1nlxP97xMcGH/MXw0qWaKURfQ==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + with@7.0.2: + resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} + engines: {node: '>= 10.0.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@alloc/quick-lru@5.2.0': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.29.2': {} + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@exodus/bytes@1.15.0': {} + + '@fortawesome/fontawesome-free@6.7.2': {} + + '@ioredis/commands@1.5.1': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kurkle/color@0.3.4': {} + + '@mongodb-js/saslprep@1.4.9': + dependencies: + sparse-bitfield: 3.0.3 + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodable/entities@2.1.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/types@0.127.0': {} + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.17': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@socket.io/component-emitter@3.1.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@tailwindcss/node@4.2.4': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.0 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.4 + + '@tailwindcss/oxide-android-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide@4.2.4': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-x64': 4.2.4 + '@tailwindcss/oxide-freebsd-x64': 4.2.4 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-x64-musl': 4.2.4 + '@tailwindcss/oxide-wasm32-wasi': 4.2.4 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + + '@tailwindcss/postcss@4.2.4': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.4 + '@tailwindcss/oxide': 4.2.4 + postcss: 8.5.12 + tailwindcss: 4.2.4 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.12.2 + + '@types/browser-sync@2.29.1': + dependencies: + '@types/micromatch': 2.3.35 + '@types/node': 24.12.2 + '@types/serve-static': 2.2.0 + chokidar: 3.6.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/compression@1.8.1': + dependencies: + '@types/express': 5.0.6 + '@types/node': 24.12.2 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.12.2 + + '@types/cookie-parser@1.4.10(@types/express@5.0.6)': + dependencies: + '@types/express': 5.0.6 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 24.12.2 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 24.12.2 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express-session@1.19.0': + dependencies: + '@types/express': 5.0.6 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/geoip-lite@1.4.4': {} + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.12.2 + + '@types/less@3.0.8': {} + + '@types/luxon@3.7.1': {} + + '@types/method-override@3.0.0(@types/express@5.0.6)': + dependencies: + '@types/express': 5.0.6 + + '@types/micromatch@2.3.35': + dependencies: + '@types/parse-glob': 3.0.32 + + '@types/morgan@1.9.10': + dependencies: + '@types/node': 24.12.2 + + '@types/ms@2.1.0': {} + + '@types/multer@1.4.13': + dependencies: + '@types/express': 5.0.6 + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + + '@types/nodemailer@6.4.23': + dependencies: + '@types/node': 24.12.2 + + '@types/numeral@2.0.5': {} + + '@types/parse-glob@3.0.32': {} + + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 24.12.2 + + '@types/serve-favicon@2.5.7': + dependencies: + '@types/express': 5.0.6 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.12.2 + + '@types/uikit@3.23.0': {} + + '@types/uuid@10.0.0': {} + + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.12.2 + + '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0) + + '@vitest/expect@4.1.5': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0) + + '@vitest/pretty-format@4.1.5': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.5': + dependencies: + '@vitest/utils': 4.1.5 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.5': {} + + '@vitest/utils@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn@7.4.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansicolor@2.0.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + append-field@1.0.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-union@2.1.0: {} + + asap@2.0.6: {} + + assert-never@1.4.0: {} + + assertion-error@2.0.1: {} + + async-each-series@0.1.1: {} + + async@2.6.4: + dependencies: + lodash: 4.18.1 + + async@3.2.6: {} + + autoprefixer@10.5.0(postcss@8.5.12): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001791 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.12 + postcss-value-parser: 4.2.0 + + babel-walk@3.0.0-canary-5: + dependencies: + '@babel/types': 7.29.0 + + balanced-match@1.0.2: {} + + base64id@2.0.0: {} + + baseline-browser-mapping@2.10.23: {} + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + batch@0.6.1: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + binary-extensions@2.3.0: {} + + block-stream2@2.1.0: + dependencies: + readable-stream: 3.6.2 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-or-node@2.1.1: {} + + browser-sync-client@3.0.4: + dependencies: + etag: 1.8.1 + fresh: 0.5.2 + mitt: 1.2.0 + + browser-sync-ui@3.0.4: + dependencies: + async-each-series: 0.1.1 + chalk: 4.1.2 + connect-history-api-fallback: 1.6.0 + immutable: 3.8.3 + server-destroy: 1.0.1 + socket.io-client: 4.8.3 + stream-throttle: 0.1.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + browser-sync@3.0.4: + dependencies: + browser-sync-client: 3.0.4 + browser-sync-ui: 3.0.4 + bs-recipes: 1.3.4 + chalk: 4.1.2 + chokidar: 3.6.0 + connect: 3.6.6 + connect-history-api-fallback: 1.6.0 + dev-ip: 1.0.1 + easy-extender: 2.3.4 + eazy-logger: 4.1.0 + etag: 1.8.1 + fresh: 0.5.2 + fs-extra: 3.0.1 + http-proxy: 1.18.1 + immutable: 3.8.3 + micromatch: 4.0.8 + opn: 5.3.0 + portscanner: 2.2.0 + raw-body: 2.5.3 + resp-modifier: 6.0.2 + rx: 4.1.0 + send: 0.19.2 + serve-index: 1.9.2 + serve-static: 1.16.3 + server-destroy: 1.0.1 + socket.io: 4.8.3 + ua-parser-js: 1.0.41 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.23 + caniuse-lite: 1.0.30001791 + electron-to-chromium: 1.5.344 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bs-recipes@1.3.4: {} + + bson@6.10.4: {} + + buffer-crc32@0.2.13: {} + + buffer-crc32@1.0.0: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + bull@4.16.5: + dependencies: + cron-parser: 4.9.0 + get-port: 5.1.1 + ioredis: 5.10.1 + lodash: 4.18.1 + msgpackr: 1.11.10 + semver: 7.7.4 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001791: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-parser@2.2.0: + dependencies: + is-regex: 1.2.1 + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cluster-key-slot@1.1.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@2.20.3: {} + + commander@9.5.0: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + 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 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + connect-history-api-fallback@1.6.0: {} + + connect-redis@8.1.0(express-session@1.19.0): + dependencies: + express-session: 1.19.0 + + connect@3.6.6: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.0 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + constantinople@4.0.1: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + + cron@4.4.0: + dependencies: + '@types/luxon': 3.7.1 + luxon: 3.7.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + dayjs@1.11.20: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.1.0: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + decode-uri-component@0.2.2: {} + + denque@2.1.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: {} + + dev-ip@1.0.1: {} + + diacritics@1.3.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctypes@1.1.0: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dotenv@16.6.1: {} + + dotenv@17.4.2: {} + + drange@1.1.1: {} + + dtp-cleantext@1.0.0: + dependencies: + diacritics: 1.3.0 + shoetest: 1.2.2 + stats-lite: 2.2.0 + striptags: 3.2.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + easy-extender@2.3.4: + dependencies: + lodash: 4.18.1 + + eazy-logger@4.1.0: + dependencies: + chalk: 4.1.2 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.344: {} + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + engine.io-client@6.6.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.18.3 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.7: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 24.12.2 + '@types/ws': 8.18.1 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.6 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@5.21.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@8.0.0: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + expect-type@1.3.0: {} + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express-session@1.19.0: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.1.0 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-xml-builder@1.1.5: + dependencies: + path-expression-matcher: 1.5.0 + + fast-xml-parser@5.7.2: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.5 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@1.1.0: {} + + finalhandler@1.1.0: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.3.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.16.0: {} + + forwarded@0.2.0: {} + + fraction.js@5.3.4: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fs-extra@3.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 3.0.1 + universalify: 0.1.2 + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + geoip-lite@1.4.10: + dependencies: + async: 2.6.4 + chalk: 4.1.2 + iconv-lite: 0.6.3 + ip-address: 5.9.4 + lazy: 1.0.11 + rimraf: 2.7.1 + yauzl: 2.10.0 + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + 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.3 + math-intrinsics: 1.1.0 + + get-port@5.1.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@16.5.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-flag@5.0.1: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + image-size@0.5.5: + optional: true + + immutable@3.8.3: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ip-address@5.9.4: + dependencies: + jsbn: 1.1.0 + lodash: 4.18.1 + sprintf-js: 1.1.2 + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.3.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.3 + + is-expression@4.0.0: + dependencies: + acorn: 7.4.1 + object-assign: 4.1.1 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number-like@1.0.8: + dependencies: + lodash.isfinite: 3.3.2 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-promise@2.2.2: {} + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-what@4.1.16: {} + + is-wsl@1.1.0: {} + + isnumber@1.0.0: {} + + jiti@2.6.1: {} + + js-stringify@1.0.2: {} + + js-tokens@4.0.0: {} + + jsbn@1.1.0: {} + + jsdom@29.1.0: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsonfile@3.0.1: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jstransformer@1.0.0: + dependencies: + is-promise: 2.2.2 + promise: 7.3.1 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + kareem@2.6.3: {} + + lazy@1.0.11: {} + + less@4.6.4: + dependencies: + copy-anything: 3.0.5 + parse-node-version: 1.0.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.5.0 + source-map: 0.6.1 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + limiter@1.1.5: {} + + lodash.defaults@4.2.0: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isfinite@3.3.2: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lodash@4.18.1: {} + + lru-cache@11.3.5: {} + + luxon@3.7.2: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + optional: true + + marked@16.4.2: {} + + math-intrinsics@1.1.0: {} + + mdn-data@2.27.1: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memory-pager@1.5.0: {} + + merge-descriptors@2.0.0: {} + + merge2@1.4.1: {} + + method-override@3.0.0: + dependencies: + debug: 3.1.0 + methods: 1.1.2 + parseurl: 1.3.3 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minio@8.0.7: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 1.0.0 + eventemitter3: 5.0.4 + fast-xml-parser: 5.7.2 + ipaddr.js: 2.3.0 + lodash: 4.18.1 + mime-types: 2.1.35 + query-string: 7.1.3 + stream-json: 1.9.1 + through2: 4.0.2 + xml2js: 0.6.2 + + mitt@1.2.0: {} + + mongodb-connection-string-url@3.0.2: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 14.2.0 + + mongodb@6.20.0: + dependencies: + '@mongodb-js/saslprep': 1.4.9 + bson: 6.10.4 + mongodb-connection-string-url: 3.0.2 + + mongoose@8.23.1: + dependencies: + bson: 6.10.4 + kareem: 2.6.3 + mongodb: 6.20.0 + mpath: 0.9.0 + mquery: 5.0.0 + ms: 2.1.3 + sift: 17.1.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + + mpath@0.9.0: {} + + mquery@5.0.0: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.10: + optionalDependencies: + msgpackr-extract: 3.0.3 + + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mylas@2.1.14: {} + + nanoid@3.3.11: {} + + needle@3.5.0: + dependencies: + iconv-lite: 0.6.3 + sax: 1.6.0 + optional: true + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-releases@2.0.38: {} + + nodemailer@7.0.13: {} + + normalize-path@3.0.0: {} + + numeral@2.0.6: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obug@2.1.1: {} + + ollama@0.6.3: + dependencies: + whatwg-fetch: 3.6.20 + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openai@6.34.0(ws@8.18.3): + optionalDependencies: + ws: 8.18.3 + + opn@5.3.0: + dependencies: + is-wsl: 1.1.0 + + parse-node-version@1.0.1: {} + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + parseurl@1.3.3: {} + + path-expression-matcher@1.5.0: {} + + path-is-absolute@1.0.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@8.4.2: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@4.0.1: + optional: true + + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + + portscanner@2.2.0: + dependencies: + async: 2.6.4 + is-number-like: 1.0.8 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.12: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@3.8.3: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + promise@7.3.1: + dependencies: + asap: 2.0.6 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + prr@1.0.1: + optional: true + + pug-attrs@3.0.0: + dependencies: + constantinople: 4.0.1 + js-stringify: 1.0.2 + pug-runtime: 3.0.1 + + pug-code-gen@3.0.4: + dependencies: + constantinople: 4.0.1 + doctypes: 1.1.0 + js-stringify: 1.0.2 + pug-attrs: 3.0.0 + pug-error: 2.1.0 + pug-runtime: 3.0.1 + void-elements: 3.1.0 + with: 7.0.2 + + pug-error@2.1.0: {} + + pug-filters@4.0.0: + dependencies: + constantinople: 4.0.1 + jstransformer: 1.0.0 + pug-error: 2.1.0 + pug-walk: 2.0.0 + resolve: 1.22.12 + + pug-lexer@5.0.1: + dependencies: + character-parser: 2.2.0 + is-expression: 4.0.0 + pug-error: 2.1.0 + + pug-linker@4.0.0: + dependencies: + pug-error: 2.1.0 + pug-walk: 2.0.0 + + pug-load@3.0.0: + dependencies: + object-assign: 4.1.1 + pug-walk: 2.0.0 + + pug-parser@6.0.0: + dependencies: + pug-error: 2.1.0 + token-stream: 1.0.0 + + pug-runtime@3.0.1: {} + + pug-strip-comments@2.0.0: + dependencies: + pug-error: 2.1.0 + + pug-walk@2.0.0: {} + + pug@3.0.4: + dependencies: + pug-code-gen: 3.0.4 + pug-filters: 4.0.0 + pug-lexer: 5.0.1 + pug-linker: 4.0.0 + pug-load: 3.0.0 + pug-parser: 6.0.0 + pug-runtime: 3.0.1 + pug-strip-comments: 2.0.0 + + punycode@2.3.1: {} + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + queue-lit@1.5.2: {} + + queue-microtask@1.2.3: {} + + randexp@0.5.3: + dependencies: + drange: 1.1.1 + ret: 0.2.2 + + random-bytes@1.0.0: {} + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + + react@19.2.5: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resp-modifier@6.0.2: + dependencies: + debug: 2.6.9 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + ret@0.2.2: {} + + reusify@1.1.0: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + + rotating-file-stream@3.2.9: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rx@4.1.0: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sax@1.6.0: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@5.7.2: + optional: true + + semver@7.7.4: {} + + send@0.19.2: + 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 + transitivePeerDependencies: + - supports-color + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-favicon@2.5.1: + dependencies: + etag: 1.8.1 + fresh: 0.5.2 + ms: 2.1.3 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + + serve-index@1.9.2: + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.8.1 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + server-destroy@1.0.1: {} + + set-cookie-parser@2.7.2: {} + + setprototypeof@1.2.0: {} + + shoetest@1.2.2: + dependencies: + randexp: 0.5.3 + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + sift@17.1.3: {} + + siginfo@2.0.0: {} + + slash@3.0.0: {} + + socket.io-adapter@2.5.6: + dependencies: + debug: 4.4.3 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.8.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-client: 6.6.4 + socket.io-parser: 4.2.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.6: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.3: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.6 + debug: 4.4.3 + engine.io: 6.6.7 + socket.io-adapter: 2.5.6 + socket.io-parser: 4.2.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + source-map-js@1.2.1: {} + + source-map@0.6.1: + optional: true + + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + + split-on-first@1.1.0: {} + + sprintf-js@1.1.2: {} + + stackback@0.0.2: {} + + standard-as-callback@2.1.0: {} + + stats-lite@2.2.0: + dependencies: + isnumber: 1.0.0 + + statuses@1.3.1: {} + + statuses@1.5.0: {} + + statuses@2.0.2: {} + + std-env@4.1.0: {} + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + stream-throttle@0.1.3: + dependencies: + commander: 2.20.3 + limiter: 1.1.5 + + streamsearch@1.1.0: {} + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + striptags@3.2.0: {} + + strnum@2.2.3: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tailwindcss@4.2.4: {} + + tapable@2.3.3: {} + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + tinybench@2.9.0: {} + + tinyexec@1.1.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tldts-core@7.0.28: {} + + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-stream@1.0.0: {} + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + tsc-alias@1.8.16: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.14.0 + globby: 11.1.0 + mylas: 2.1.14 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typescript@5.9.3: {} + + typescript@6.0.3: {} + + ua-parser-js@1.0.41: {} + + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + + uikit@3.25.16: {} + + undici-types@7.16.0: {} + + undici-types@7.19.2: {} + + undici@7.25.0: {} + + universalify@0.1.2: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + uuid@8.3.2: {} + + vary@1.1.2: {} + + vite@8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.12 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.2 + esbuild: 0.25.12 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.6.4 + tsx: 4.21.0 + + vitest@4.1.5(@types/node@24.12.2)(jsdom@29.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.10(@types/node@24.12.2)(esbuild@0.25.12)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.2 + jsdom: 29.1.0 + transitivePeerDependencies: + - msw + + void-elements@3.1.0: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + webidl-conversions@8.0.1: {} + + whatwg-fetch@3.6.20: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + with@7.0.2: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + assert-never: 1.4.0 + babel-walk: 3.0.0-canary-5 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + xml-name-validator@5.0.0: {} + + xml2js@0.6.2: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlchars@2.2.0: {} + + xmlhttprequest-ssl@2.1.2: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..fda4af6 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - 'packages/*' + - 'gadget-code' + - 'gadget-drone' \ No newline at end of file