From 941abd98d56409c57ba0ea9979fc5b756a51da49 Mon Sep 17 00:00:00 2001 From: Arnaud Nelissen Date: Sun, 28 Jun 2026 01:05:24 +0200 Subject: [PATCH] feat: initial myprimal MQTT bridge service Dynamic MySolem/MyIndygo IoT bridge with MQTT, Home Assistant autodiscovery, and Homebridge EasyMQTT compatibility. - Device discovery via MySolem API on startup (all modules/inputs/outputs) - Configurable poll engine (fast/normal/slow buckets, per-input overrides) - MQTT retained state publishing + HA autodiscovery payloads - Unified manual command routing (linesControl vs sendManualModuleCommand) - Runtime config via MQTT $config/set, persisted to config.json - Expression override system (API-provided > hardcoded > passthrough) - Minimal REST API: /health, /state, /control, /rediscover - Docker deployment (Dockerfile + docker-compose.yml) - 38 unit tests Co-Authored-By: Claude Sonnet 4.6 --- .dockerignore | 8 + .env.example | 22 + .gitignore | 10 + CLAUDE.md | 36 + Dockerfile | 7 + README.md | 200 + api-docs/REFERENCE.md | 1366 +++++++ api-docs/control.md | 258 ++ api-docs/endpoints.md | 485 +++ api-docs/lrag.md | 101 + api-docs/lripeco.md | 79 + api-docs/lrmb10.md | 32 + api-docs/lrms.md | 73 + api-docs/lrniv.md | 82 + api-docs/lrpc.md | 121 + api-docs/lrpr.md | 66 + api-docs/lrps.md | 69 + api-docs/mobile-api.md | 343 ++ api-docs/myindygo.md | 367 ++ api-docs/overview.md | 139 + api-docs/pool-level-sensor.md | 53 + api-docs/security-critique.md | 172 + api-docs/write-endpoints.md | 249 ++ docker-compose.yml | 12 + package-lock.json | 6438 +++++++++++++++++++++++++++++++++ package.json | 23 + src/api/routes.js | 61 + src/api/server.js | 17 + src/config.js | 57 + src/control.js | 93 + src/index.js | 50 + src/input-types.js | 23 + src/module-types.js | 38 + src/mqtt/client.js | 61 + src/mqtt/commands.js | 56 + src/mqtt/config-bridge.js | 41 + src/mqtt/discovery.js | 76 + src/mqtt/publish.js | 21 + src/normalize.js | 49 + src/poller.js | 109 + src/registry.js | 159 + src/solem.js | 81 + src/state.js | 33 + test/config.test.js | 76 + test/input-types.test.js | 33 + test/module-types.test.js | 49 + test/normalize.test.js | 82 + test/state.test.js | 42 + 48 files changed, 12118 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 api-docs/REFERENCE.md create mode 100644 api-docs/control.md create mode 100644 api-docs/endpoints.md create mode 100644 api-docs/lrag.md create mode 100644 api-docs/lripeco.md create mode 100644 api-docs/lrmb10.md create mode 100644 api-docs/lrms.md create mode 100644 api-docs/lrniv.md create mode 100644 api-docs/lrpc.md create mode 100644 api-docs/lrpr.md create mode 100644 api-docs/lrps.md create mode 100644 api-docs/mobile-api.md create mode 100644 api-docs/myindygo.md create mode 100644 api-docs/overview.md create mode 100644 api-docs/pool-level-sensor.md create mode 100644 api-docs/security-critique.md create mode 100644 api-docs/write-endpoints.md create mode 100644 docker-compose.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/api/routes.js create mode 100644 src/api/server.js create mode 100644 src/config.js create mode 100644 src/control.js create mode 100644 src/index.js create mode 100644 src/input-types.js create mode 100644 src/module-types.js create mode 100644 src/mqtt/client.js create mode 100644 src/mqtt/commands.js create mode 100644 src/mqtt/config-bridge.js create mode 100644 src/mqtt/discovery.js create mode 100644 src/mqtt/publish.js create mode 100644 src/normalize.js create mode 100644 src/poller.js create mode 100644 src/registry.js create mode 100644 src/solem.js create mode 100644 src/state.js create mode 100644 test/config.test.js create mode 100644 test/input-types.test.js create mode 100644 test/module-types.test.js create mode 100644 test/normalize.test.js create mode 100644 test/state.test.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6a2f443 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +config.json +.env +api-docs +client +server +inputs +tools diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dba2793 --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# MySolem credentials +SOLEM_EMAIL=your@email.com +SOLEM_PASSWORD=yourpassword +SOLEM_BASE=https://qualif.mysolem.com +INDYGO_BASE=https://qualif.myindygo.com + +# Optional: override user ID (default from CLAUDE.md) +# SOLEM_USER_ID=5de53bca7ed4c45c2cfe067f + +# MQTT broker +MQTT_URL=mqtt://localhost:1883 +MQTT_USERNAME= +MQTT_PASSWORD= +MQTT_TOPIC_PREFIX=myprimal + +# REST API port +PORT=3001 + +# Poll intervals (seconds) — also tunable at runtime via MQTT $config/set +POLL_FAST=60 +POLL_NORMAL=300 +POLL_SLOW=900 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8eec4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +.env +config.json +*.log + +# Legacy directories (not part of new service) +client/ +server/ +inputs/ +tools/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4e37130 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# myprimal + +Reverse-engineered client for the MySolem irrigation platform. + +## Goal + +Fetch sensor telemetry and control IoT devices (valves, stations) by talking directly to the MySolem backend. + +## Target environment + +**Base URL**: `https://qualif.mysolem.com` (qualification environment — always use this, not www.mysolem.com) + +## Project layout + +``` +api-docs/ # Reverse-engineered API documentation + overview.md # Auth, architecture, key IDs + endpoints.md # All discovered endpoints + control.md # Device control commands +``` + +## Auth + +Session-cookie based (Sails.js). Call `POST /login` with form data, keep the cookie jar for all subsequent requests. See `api-docs/overview.md`. + +## Key account IDs + +- User ID: `5de53bca7ed4c45c2cfe067f` +- Site ID (Domicile): `62b30d26ecbceb08607592c1` +- Soil moisture input ID: `60ca42adad69dac1558f7c66` + +## Notes + +- Backend: Node.js + Sails.js, MongoDB ObjectIds +- Real-time updates via socket.io, but all read/write ops also work over plain HTTP +- No CSRF enforcement on JSON endpoints diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4c86a46 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY src/ ./src/ +EXPOSE 3001 +CMD ["node", "src/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c460222 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# myprimal + +MQTT bridge for the MySolem / MyIndygo IoT platform (irrigation + pool). Reverse-engineered client that dynamically discovers all devices and exposes them via MQTT with Home Assistant autodiscovery and Homebridge EasyMQTT compatibility. + +## Features + +- Auto-discovers all modules, sensors, and outputs on startup +- Polls sensor readings on configurable intervals +- Publishes to MQTT with retained state +- Home Assistant MQTT autodiscovery (sensors + switches) +- Compatible with Homebridge [EasyMQTT](https://github.com/Shaquu/homebridge-easymqtt) plugin +- Manual commands via MQTT (`/set` topics) +- Runtime poll interval tuning via MQTT +- REST API for health, state snapshot, and direct control +- Docker deployment + +## Setup + +### 1. Configure + +Copy `.env.example` to `.env` and fill in credentials: + +```bash +cp .env.example .env +``` + +Required: +- `SOLEM_EMAIL` / `SOLEM_PASSWORD` — MySolem account credentials +- `MQTT_URL` — your MQTT broker (e.g. `mqtt://192.168.1.10:1883`) + +### 2. Run locally + +```bash +npm install +node src/index.js +``` + +### 3. Run with Docker + +```bash +docker compose up -d +``` + +On first run, `config.json` is created in the Docker volume for persistent poll interval settings. + +## MQTT Topics + +All topics are prefixed with `MQTT_TOPIC_PREFIX` (default: `myprimal`). + +### Sensor readings (retained) + +``` +myprimal/{module_slug}/sensor/{metric} +``` + +Payload: `{"value": 24.5, "unit": "°C", "ts": "2026-06-28T10:00:00.000Z"}` + +Examples: +``` +myprimal/lrpc_f1da27/sensor/temperature {"value": 24.5, "unit": "°C", ...} +myprimal/lrps_0565c0/sensor/ph {"value": 7.35, "unit": "pH", ...} +myprimal/lrps_0565c0/sensor/orp_redox {"value": 690, "unit": "mV", ...} +myprimal/lr6ag_070917/sensor/plumbago {"value": 54, "unit": "%", ...} +``` + +### Output state (retained) + +``` +myprimal/{module_slug}/output/{index}/state +``` + +Payload: `{"value": 0, "ts": "..."}` — `value` 1 = running, 0 = stopped + +### Commands + +``` +myprimal/{module_slug}/output/{index}/set +``` + +Payload: +```json +{ "action": "run", "duration": 30 } // run for 30 minutes +{ "action": "stop" } // stop immediately +{ "action": "boost", "duration": 120 } // boost (pool pump only) +{ "action": "pause", "days": 3 } // pause/rain delay N days +``` + +### Availability (retained) + +``` +myprimal/{module_slug}/availability "online" | "offline" +``` + +### Config (retained, runtime-tunable) + +Read current config: +``` +myprimal/$config +``` + +Update poll intervals (seconds): +``` +myprimal/$config/set {"poll": {"fast": 30, "normal": 120}} +``` + +Update interval for a specific sensor: +``` +myprimal/$config/set {"poll": {"overrides": {"lrps_0565c0/ph": 30}}} +``` + +Changes take effect immediately and persist across restarts. + +### Poll buckets (defaults) + +| Bucket | Default | Sensor types | +|---|---|---| +| `fast` | 60s | pH, ORP, pressure, water temperature | +| `normal` | 300s | Soil moisture, air temperature, flow | +| `slow` | 900s | Binary sensors (rain, level switch, valve state) | + +## Home Assistant + +Entities are auto-created via MQTT discovery on startup. Check **Settings → Devices & Services → MQTT** — all Solem devices appear automatically. + +Discovery topics: `homeassistant/{sensor|binary_sensor|switch}/myprimal_{id}/config` + +No manual configuration required. + +## Homebridge (EasyMQTT) + +Install [homebridge-easymqtt](https://github.com/Shaquu/homebridge-easymqtt) and configure accessories pointing to the MQTT topics above. + +Example for pool light switch: +```json +{ + "accessory": "EasyMQTT", + "name": "Pool Light", + "type": "Switch", + "mqttGetTopic": "myprimal/lrpc_f1da27/output/1/state", + "mqttSetTopic": "myprimal/lrpc_f1da27/output/1/set", + "mqttGetTransformation": "return JSON.parse(message).value === 1 ? 'true' : 'false';", + "mqttSetTransformation": "return value === 'true' ? JSON.stringify({action:'run',duration:30}) : JSON.stringify({action:'stop'});" +} +``` + +## REST API + +| Method | Path | Description | +|---|---|---| +| `GET` | `/health` | Service health + counts | +| `GET` | `/state` | All cached sensor readings | +| `GET` | `/state/:moduleId` | Readings for one module | +| `GET` | `/registry` | Full device registry | +| `POST` | `/control/:moduleId/:outputIndex` | Send manual command | +| `POST` | `/rediscover` | Re-discover all devices | + +### Control example + +```bash +# Run pool light (LRPC output 1) for 30 minutes +curl -X POST http://localhost:3001/control/6481bcea8782b78554c02bed/1 \ + -H 'Content-Type: application/json' \ + -d '{"action":"run","duration":30}' + +# Stop +curl -X POST http://localhost:3001/control/6481bcea8782b78554c02bed/1 \ + -H 'Content-Type: application/json' \ + -d '{"action":"stop"}' +``` + +## Module Slug Reference (Domicile) + +| Module | Slug | Type | +|---|---|---| +| LRPC-F1DA27 (pool controller) | `lrpc_f1da27` | Filtration pump (output 0), light (output 1) | +| LR6AG-070917 (irrigation) | `lr6ag_070917` | 6 irrigation stations (outputs 0–5) | +| LRMS-0721DC (sensor station) | `lrms_0721dc` | Soil moisture × 2, air temp | +| LRPS-0565C0 (pool analyser) | `lr_mas_0565c0` | Water temp, pH, ORP | +| LRPR-0D6800 (filter pressure) | `lr_pr_0d6800` | Filter pressure | +| LRLEVEL-0D2ED9 (fill valve) | `lr_niv_0d2ed9` | Fill valve control | +| LR2IPECO-0C9282 | `lr_ip_eco_0c9282` | Flow meter | +| POOL-LVL-0E953B (level switch) | `pool_level_sensor_0e953b` | Pool level binary | + +## Architecture + +``` +MySolem API ──► solem.js (auth + HTTP) + │ + registry.js (device discovery on startup) + │ + poller.js (per-bucket poll timers) + │ + state.js (in-memory cache, EventEmitter) + │ + ┌─────────┴──────────┐ + mqtt/publish.js api/routes.js + mqtt/discovery.js (REST API) + mqtt/commands.js ──► control.js ──► MySolem API + mqtt/config-bridge.js +``` diff --git a/api-docs/REFERENCE.md b/api-docs/REFERENCE.md new file mode 100644 index 0000000..f9b0e95 --- /dev/null +++ b/api-docs/REFERENCE.md @@ -0,0 +1,1366 @@ +# MySolem / MyIndygo — Complete API Reference + +Reverse-engineered from black-box analysis: mitmproxy captures of iOS apps, Safari Web Inspector recordings, JS bundle static analysis, and live endpoint probing. + +**Sources**: `inputs/apps_captured_all.jsonl`, `inputs/lrpc_post.json`, `inputs/mysolem_captured_all.jsonl`, `inputs/indygo_captured_all.jsonl`, `inputs/captured_all.jsonl`, JS bundles at `/min/production.min.js` and `/linker/js/esbuild-bundle.js`. + +--- + +## Table of Contents + +1. [Platforms & Architecture](#1-platforms--architecture) +2. [Authentication](#2-authentication) +3. [Key IDs — Domicile Site](#3-key-ids--domicile-site) +4. [Web API — Endpoints](#4-web-api--endpoints) +5. [Mobile API — /api/ Endpoints](#5-mobile-api--api-endpoints) +6. [Device Control](#6-device-control) +7. [MyIndygo — Pool Platform](#7-myindygo--pool-platform) +8. [Device Reference — Domicile](#8-device-reference--domicile) +9. [Write Endpoints Reference](#9-write-endpoints-reference) +10. [Reference Tables](#10-reference-tables) +11. [Security Findings](#11-security-findings) + +--- + +## 1. Platforms & Architecture + +| Platform | Base URL | Focus | +|---|---|---| +| MySOLEM | `https://qualif.mysolem.com` | Irrigation, garden sensors | +| MyIndygo | `https://qualif.myindygo.com` | Pool control, water chemistry | + +Both platforms share **the same backend database and API server** — same module IDs, input IDs, session cookie, and sensor data. MyIndygo adds a pool admin/management layer on top. All web endpoints work identically on either host. + +**Backend**: Node.js + Sails.js v0.13.8, MongoDB (all IDs are 24-char hex ObjectIds) +**Proxy**: nginx/1.18.0 (Ubuntu) +**Real-time**: Socket.IO v3 / Engine.IO v3 (`EIO=3`), direct WebSocket transport (not long-polling) +``` +wss://qualif.mysolem.com/socket.io/?EIO=3&transport=websocket +``` + +**Two separate API layers on the same host**: +- **Web API** — no path prefix, session cookie auth +- **Mobile API** — `/api/` prefix, OAuth2 Bearer token auth + +--- + +## 2. Authentication + +### 2.1 Web API — Session Cookie + +Cookie-based session (Sails.js). No CSRF token required on JSON endpoints. + +```bash +# Step 1 — init cookie jar +curl -c cookies.txt https://qualif.mysolem.com/login + +# Step 2 — authenticate +curl -c cookies.txt -b cookies.txt \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "email=YOUR_EMAIL&password=YOUR_PASSWORD" \ + "https://qualif.mysolem.com/login" +# → 302 redirect to / on success + +# All subsequent calls +curl -b cookies.txt https://qualif.mysolem.com/features +``` + +**Cookie name**: `qualification-mysolem-solem-irrigation-platform.sid` +**Session TTL**: ~7 days +Same cookie works on both `qualif.mysolem.com` and `qualif.myindygo.com`. + +**Python:** +```python +import requests + +BASE = "https://qualif.mysolem.com" +s = requests.Session() +s.get(f"{BASE}/login") # init cookie +s.post(f"{BASE}/login", data={ + "email": "YOUR_EMAIL", + "password": "YOUR_PASSWORD", +}) # 302 → / on success +# s carries the session cookie automatically on all subsequent calls +``` + +### 2.2 Mobile API — OAuth2 Bearer Token + +The iOS apps use a completely separate auth layer at `/api/` prefix. + +``` +POST /oauth2/token +Content-Type: application/json + +{"scope": "*", "grant_type": "client_credentials"} +``` + +Response: +```json +{ + "access_token": "6wtff8dVlhjn7y...", + "expires_in": 5184000, + "token_type": "Bearer" +} +``` + +`expires_in` = 5,184,000 seconds = **60 days**. + +The `client_id` and `client_secret` are embedded in the app binary and validated server-side — not present as explicit headers in captured traffic. + +All subsequent mobile API requests: +``` +Authorization: Bearer {access_token} +``` + +**Python:** +```python +import requests + +BASE = "https://qualif.mysolem.com" +r = requests.post(f"{BASE}/oauth2/token", + json={"scope": "*", "grant_type": "client_credentials"}) +token = r.json()["access_token"] +headers = {"Authorization": f"Bearer {token}"} + +resp = requests.get(f"{BASE}/api/getUser", headers=headers) +``` + +--- + +## 3. Key IDs — Domicile Site + +### Users + +| Platform | Account | User ID | +|---|---|---| +| MySOLEM | arnaud.nelissen.api@icloud.com | `5de53bca7ed4c45c2cfe067f` | +| MyIndygo | arnaud.nelissen.api@icloud.com | `6a21c3db5ebe0fa5f7885460` | + +### Site + +| Resource | ID | +|---|---| +| Site (Domicile) | `62b30d26ecbceb08607592c1` | +| Pool (Domicile) | `5de53c1c7ed4c45c2cfe0688` | +| Field supervision | `643c5134347b27edbf38b173` | + +### Modules + +| Module | Type | Module ID | Serial Number | childSerial6 | +|---|---|---|---|---| +| LRMB10-070532 (gateway) | `lr-mb-10` | `60ca4276ad69dac1558f7c61` | `1000000705320001` | — | +| LRMS-0721DC (sensor station) | `lr-ms` | `60ca42adad69dac1558f7c64` | `7400040721DC0001` | `0721DC` | +| LR6AG-070917 (irrigation valve) | `lr-ag` | `626984f038ec558c598bb6d5` | `6606010709170001` | `070917` | +| LRPS-0565C0 (pool analyser) | `lr-mas` | `6480da07c2c0896da120ecca` | `9400030565C00001` | `0565C0` | +| LRPC-F1DA27 (pool controller) | `lr-pc` | `6481bcea8782b78554c02bed` | `150302F1DA270001` | `F1DA27` | +| LR2IPECO-0C9282 (IP ECO) | `lr-ip-eco` | `6516ca57b7c098e6807cb332` | `3802020C92820001` | `0C9282` | +| LRPR-0D6800 (filter pressure) | `lr-pr` | `6a17218be0b88ea3e925524f` | `1D00010D68000001` | `0D6800` | +| POOL-LVL-0E953B (float switch) | `pool-level-sensor` | `6a174dd1e0b88ea3e925531c` | `0300020E953B0001` | — | +| LRLEVEL-0D2ED9 (fill valve) | `lr-niv` | `6a1c4b153d6c33ab843edc13` | `3A01020D2ED90001` | `0D2ED9` | + +All child modules route through gateway `1000000705320001` (LRMB10) via LoRa radio. + +### Inputs + +| Sensor | Module | Input ID | Type | Unit | +|---|---|---|---|---| +| Plumbago moisture | LRMS | `60ca42adad69dac1558f7c66` | 12 | % | +| Pelouse moisture | LRMS | `60ca42adad69dac1558f7c67` | 12 | % | +| Air temperature | LRMS | `60ca42adad69dac1558f7c69` | 5 | °C | +| Rain sensor | LR6AG | `626984f038ec558c598bb6d7` | 2 | 0/1 | +| Pool water temperature | LRPS | `6480da07c2c0896da120eccc` | 133 | °C | +| Pool pH | LRPS | `6480da07c2c0896da120eccd` | 6 | pH (raw ÷ 100) | +| Pool ORP / Redox | LRPS | `6480da07c2c0896da120ecce` | 7 | mV | +| Flow meter | LR2IPECO | `6516ca57b7c098e6807cb334` | 33 | L | +| Filter pressure | LRPR | `6a17218be0b88ea3e9255251` | 8 | bar | +| Pool level switch | POOL-LVL | `6a174dd1e0b88ea3e925531e` | 21 | 0/1 | +| Fill volume | LRLEVEL | `6a1c4b153d6c33ab843edc15` | 33 | L | +| Fill valve state | LRLEVEL | `6a1c4b153d6c33ab843edc16` | 149 | 0/1 | +| LRPC temperature | LRPC | `6481bcea8782b78554c02bef` | 133 | °C | + +### Outputs + +| Output | Module | Output ID | Index | Flow rate | +|---|---|---|---|---| +| AG Station 1 | LR6AG | `626984f038ec558c598bb6d8` | 0 | 30 m³/h | +| AG Station 2 | LR6AG | `626984f038ec558c598bb6d9` | 1 | 14 m³/h | +| AG Station 3 | LR6AG | `626984f038ec558c598bb6da` | 2 | 20 m³/h | +| AG Station 4 | LR6AG | `626984f038ec558c598bb6db` | 3 | 1 m³/h | +| AG Station 5 | LR6AG | `626984f038ec558c598bb6dc` | 4 | 38 L/h | +| AG Station 6 | LR6AG | `626984f038ec558c598bb6dd` | 5 | 38 L/h | +| LRPC Station 1 (filtration pump) | LRPC | `6481bcea8782b78554c02bf1` | 0 | — | +| LRPC Station 2 (pool light) | LRPC | `6481bcea8782b78554c02bf2` | 1 | — | +| LRPC Station 3 | LRPC | `6481bcea8782b78554c02bf3` | 2 | — | +| IPECO Station 1 | LR2IPECO | `6516ca57b7c098e6807cb336` | 0 | — | +| IPECO Station 2 | LR2IPECO | `6516ca57b7c098e6807cb337` | 1 | — | +| LRLEVEL fill valve | LRLEVEL | `6a1c4b153d6c33ab843edc17` | 0 | — | + +### Programs + +| Program | Module | Program ID | Index | Type | +|---|---|---|---|---| +| Filtration (4h/day) | LRPC | `6481bcea8782b78554c02bf4` | 0 | 4 (filtration) | +| Auxiliary/cyclic | LRPC | `6481bcea8782b78554c02bf5` | 1 | 2 (auxiliary) | +| Unused | LRPC | `6481bcea8782b78554c02bf6` | 2 | — | + +--- + +## 4. Web API — Endpoints + +All paths relative to `https://qualif.mysolem.com` (or `https://qualif.myindygo.com` — identical backend). All require session cookie. + +### 4.1 Feature Flags + +``` +GET /features +``` +Returns flat JSON object of boolean flags: `dashboard`, `enableHydraulicSupervision`, `enableBSTSubscriptions`, `enablePolytropicAPI`, `enableGraphicalProgrammation`, `enableNdviSatellite`, `enableEvapotranspiration`, `ocedisWaterChemistryCorrection`, etc. + +### 4.2 Sites + +``` +GET /canopy-sites?userId={userId} +``` +Returns `{ items: [...], total: N }`. Each item includes full `modules` array and `fencing` config. + +``` +GET /canopy-sites/{siteId}/users +GET /canopy-sites/{siteId}/modules?limit=20&skip=0&includeTotalCount=true +GET /canopy-sites/{siteId}/modules/indicators +GET /canopy-sites/{siteId}/informations +GET /canopy-sites/{siteId}/rules +GET /canopy-sites/{siteId}/map/modules +GET /canopy-sites/{siteId}/modules/eco-mode-compatible +GET /canopy-sites/{siteId}/site-notes +GET /canopy-sites/{siteId}/notifications +GET /canopy/{siteId}/waterBudget +``` + +### 4.3 Modules + +**Search / list (MongoDB-style filter):** +``` +POST /modules/search +Content-Type: application/json + +{ + "query": { "owner": "userId" }, + "projection": { "name": 1, "type": 1, "serialNumber": 1 } +} +``` +Response: `{ result: { results: [...], totalCount: null } }` +⚠ Empty query `{}` returns **all 4,607 platform modules** — no ownership enforcement. + +``` +GET /modules/available?userId={userId} +GET /modules/echarts-data?includeCustomers=true&includeAgency=false +→ {chartModuleTypes: [{name, value}], chartModuleConnectivities: [...]} + +GET /sim-cards/needsPayment?moduleIds={id1}&moduleIds={id2} +→ {result: {"moduleId": false, ...}} +``` + +**Flexible module list:** +``` +POST /users/{userId}/modules +Content-Type: application/json + +{ + "fields": {"name": 1, "type": 1, "serialNumber": 1, "displayType": 1}, + "filters": {}, + "addOwnershipFlag": true, + "includeTotalCount": true, + "limit": 20, + "skip": 0 +} +``` +Response: `{modules: [...], totalCount: N}` + +**Remote state (cached + live LoRa):** +``` +GET /remote/module/state?moduleId={moduleId} +GET /remote/module/configuration?moduleId={moduleId} +GET /remote/module/configuration/and/programs?moduleId={moduleId} +GET /remote/module/informations?moduleId={relayId}&childrenSerialNumber={childSerial} +``` +Live endpoints trigger a LoRa radio roundtrip — returns `module_not_reachable` if device is offline. + +**Module metadata:** +``` +GET /modules/{moduleId}/users +GET /modules/{moduleId}/tags +GET /modules/{moduleId}/notebook?startDate={unix_epoch_seconds} +GET /input/getInputsLinkedModule/{moduleId} +``` + +**User avatar:** +``` +GET /user/avatar → image/png (no-cache) +GET /user/avatar?t={ms} → cache-bust variant +``` + +### 4.4 Sensor Data (Inputs) + +**Batch input config:** +``` +GET /inputs?inputId=id1%3Bid2%3Bid3 +``` +Semicolons (`%3B`) separate multiple IDs. Returns full input objects with `expression`, `type`, `unit`, `module`, `index`. + +**All inputs for a module (most efficient for dashboard loads):** +``` +GET /remote/module/getComputedSensorsData + ?moduleId={moduleId} + &startDate={iso8601} + &endDate={iso8601} + &forceRawOrComputed=computed +``` +Returns array of input objects, each with `computedSensorData` ticks. Pass `forceRawOrComputed=raw` for raw ADC values. + +**Single input time-series:** +``` +GET /remote/input/getComputedSensorData + ?inputId={inputId} + &startDate={iso8601} + &endDate={iso8601} + &ensurePrevTickForContinuity=true +``` +Response: +```json +{ + "id": "60ca42adad69dac1558f7c66", + "name": "Plumbago", + "type": 12, + "unit": 10, + "interval": 60, + "computedSensorData": [ + { "tickTimestamp": "2026-06-01T00:42:00.000Z", "value": 54.1875 } + ] +} +``` + +**Last tick only (fastest — no date range needed):** +``` +GET /input/getLastTickFromInput?inputId={inputId} +``` +Returns `{ "tick": { "input": "...", "value": , "timestamp": "...", "id": "..." } }`. +Value is **raw** (pre-expression). Apply the input's `expression` to get the computed value. +LRPS pH: `raw ÷ 100` → pH. LRPS temp: `raw ÷ 100` → °C. + +**Averages & CSV:** +``` +GET /inputs/average?inputIds={id1},{id2}&startDate={iso8601}&endDate={iso8601} +GET /input/getComputedInputIpxDataBetweenDatesAsCsv?inputId={id}&startDate={iso8601}&endDate={iso8601} +``` + +**Live pool sensor snapshot (Python):** +```python +POOL_INPUTS = { + "temp": "6480da07c2c0896da120eccc", + "pH": "6480da07c2c0896da120eccd", + "ORP": "6480da07c2c0896da120ecce", + "pressure": "6a17218be0b88ea3e9255251", + "level": "6a174dd1e0b88ea3e925531e", +} +readings = {} +for name, input_id in POOL_INPUTS.items(): + r = s.get(f"{BASE}/input/getLastTickFromInput", params={"inputId": input_id}).json() + readings[name] = r.get("tick", {}).get("value") +# pH: readings["pH"] / 100 → pH units +# temp: readings["temp"] / 100 → °C +``` + +### 4.5 Outputs + +``` +GET /outputs/modules?moduleIds={moduleId} +POST /outputs/search + {"query": {"module": {"$in": ["moduleId"]}}} +POST /output/update + +GET /output/events?moduleId={moduleId} +GET /output/events?outputId={outputId}&startDate={iso8601}&endDate={iso8601} +GET /output/forecast?moduleId={moduleId} +GET /output/forecast?outputId={outputId} +``` +Response structure: `{ outputs: [{ id, module, index, name, flowRate: { value, unit } }] }` +Flow rate unit: `1` = L/h, `2` = m³/h. + +### 4.6 Programs + +``` +GET /programs/modules?moduleIds={moduleId} +PUT /programs {"programs": [...], "module": "moduleId"} +POST /programs create new program +DELETE /programs +PATCH /programs/name +POST /programs/search {"query": {"id": {"$in": [...]}}} +GET /programs/reset?moduleId={moduleId} +POST /checkAvailableWindows check time conflicts before saving +``` + +**Program object:** +```json +{ + "id": "...", + "name": "Pelouse Terrasse", + "module": "moduleId", + "index": 0, + "programmingType": "temporal", + "weekDays": 127, + "waterBudget": 100, + "windows": [{ "start": 420, "end": 422, "on": 120, "off": 0 }] +} +``` +Window times are **minutes from midnight**. `on` / `off` are in **seconds**. `weekDays` 127 = every day (bitmask, bit 0 = Monday). + +**LRPC extended program fields** (pool controller): +```json +{ + "programCharacteristics": { + "programType": 4, + "mode": 2, + "onSpeed": 1, + "speedSequence": 4368, + "microFilteringTemperature": 5, + "microFilteringDuration": 10, + "microFilteringPeriod": 480, + "frostFreeSpeed": 1 + }, + "windows": [{ "start": 300, "end": 360, "on": 3600, "off": 0, "runningDays": 127 }] +} +``` +`programType`: 4 = filtration, 2 = auxiliary/cyclic, 0 = disabled. + +### 4.7 Users + +``` +GET /users-info/{userId}?includeUserGroups=true +GET /users/{userId}/modules +GET /users/{userId}/sites +GET /users/{userId}/module-groups +GET /users/{userId}/subscriptionModules +GET /users/{userId}/checkSubscriptionSimcardStatus +POST /users/search {"query": {...}, "projection": {...}} +``` + +**Grouped resources (paginated POST):** +``` +POST /users/{userId}/module-groups → {moduleGroups, totalCount} +POST /users/{userId}/agricultural-module-groups → {agriculturalModuleGroups, totalCount} +POST /users/{userId}/watering-module-groups → {wateringModuleGroups, totalCount} +POST /users/{userId}/customers → {customers, totalCount} +POST /users/{userId}/associated → {users, totalCount} +POST /users/{userId}/tags → {tags} +``` + +**Activity log:** +``` +POST /users/{userId}/records +{ + "limit": 10, "skip": 0, + "filters": {"startDate": "2026-05-28T21:51:48.266Z", "showAllEvents": true}, + "includeTotalCount": true, + "sortColumn": "timestamp", "sortOrder": -1, + "fields": {"category": 1, "label": 1, "module": 1, "timestamp": 1} +} +→ {records: [...], totalCount: N} + +POST /users/{userId}/records/categories +{"filters": {"startDate": "..."}, "includeCustomers": true} +→ [{category, displayCategory}] +``` + +### 4.8 Module Notebook + +Full chronological history of manual commands, alerts, firmware updates, pool events: + +``` +GET /modules/{moduleId}/notebook?startDate={unix_epoch_seconds} +``` + +`startDate` is a **Unix timestamp in seconds** (not ISO 8601). + +Response: `{ "records": [...] }` sorted oldest-first. + +```json +{ + "category": "manual_command", + "label": "...", + "priority": "low", + "state": "archived", + "timestamp": "2026-06-04T21:00:00.000Z", + "params": { "action": 2, "index": 1, "manualDuration": 1800 }, + "input": "inputId" +} +``` + +Known `category` values: `manual_command`, `pool`, `platform_alert`, `firmware_update`. +For `pool` category: `params.poolAlertType` = `backwashReminder`, `backwashDone`, `poolLevelSensorCleaningReminder`, etc. +For `manual_command`: `params` mirrors the linesControl action/index that triggered the event. Note: `manualDuration` in notebook is stored in **seconds** internally, regardless of the `time:"MM:SS"` format sent to the API. + +### 4.9 Notifications & Journal + +``` +GET /journal/notifications +GET /journal/counts +GET /journal/warnings/accepted +PUT /journal/acknowledge/all +POST /canopy-sites/{siteId}/notifications/acknowledge-all +``` + +**Filtered site notifications:** +``` +POST /canopy-sites/{siteId}/notifications +{"limit": 1, "includeTotalCount": true, "filters": {"state": {"$in": ["unread", "process"]}}} +→ {records: [...]} +``` + +**Cross-site journal:** +``` +POST /journal/notifications +{ + "canopyIds": ["siteId"], + "limit": 100, "skip": 0, + "filters": {"priority": {"$in": ["critical", "high"]}, "state": {"$in": ["unread", "process"]}}, + "projection": {...} +} +→ {records: [...], total: N} +``` + +### 4.10 Weather + +``` +GET /moduleGroups/getCanopyWeather + ?location[latitude]={lat}&location[longitude]={lng} +``` +Returns 3-day AccuWeather-style forecast. + +### 4.11 Field Supervision + +``` +GET /canopy/{siteId}/field-supervisions/modules +→ {canopyModules: [{type, modules: [{serialNumber, name, outputs: [{id, index, name}]}]}]} + +GET /canopy/{siteId}/field-supervision/{supervisionId}/resume +→ {allPrograms: [...]} + +GET /moduleGroups/{siteId}/field-supervision/{supervisionId}/events/{startISO}/{endISO}?timezone=Europe/Paris +→ {events: [{eventId, moduleId, moduleName, identifier, name, start, end, flowRate, index}]} +``` + +Field supervision ID for Domicile: `643c5134347b27edbf38b173` + +### 4.12 Satellite / NDVI Imagery + +Host: **`sentinel.mysolem.com`** (separate subdomain). + +``` +GET /moduleGroups/{siteId}/fencing/{timestampMs}/images-check +→ {availableImages: {available: true, reason: "ok"}} + +GET /moduleGroups/{siteId}/fencing/{timestampMs}/images +→ {imagesList: {images: [{name, urlRgb, urlSavi}]}} + +GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/savi → PNG (vegetation index) +GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/rgb → PNG (true color) +``` + +Timestamp format: `YYYYMMDDTHHmmss`, e.g. `20220417T103631`. +Domicile plot ID: `62f4efcb4a6dd563203558c3`, fencing timestamp: `1656939129360`. + +### 4.13 Clusters & Daily Data + +``` +GET /clusters/{clusterId}/data +GET /clusters/{clusterId}/modules +GET /clusters/{clusterId}/programs +PUT /clusters/{clusterId}/programs +PATCH /clusters/{clusterId}/name +PATCH /clusters/{clusterId}/flowrate +DELETE /clusters/{clusterId} + +GET /garden/dailyData/{moduleId} +GET /modules/heatmap-data?moduleIds={ids}&startDate=X&endDate=Y +``` + +### 4.14 Pool-Specific (MyIndygo) + +``` +GET /user/pools → current user's pools (⚠ returns [] for API test account) +GET /user/pools?userId={id} → another user's pools (IDOR — no ownership check) +GET /maintenance-logs-data?moduleId={moduleId} +``` + +`maintenance-logs-data` returns `{ code, messages: [...] }`. Each message has `poolAlertType`, `priority`, `state`, `timestamp`, and a `pool` object with full pool config and `pool.status` (live sensor values, last pressure, last pH, etc.). + +**Pool admin (admin-only, 403 on standard account):** +``` +GET /pools/admin +POST /pools/admin/search +POST /pools/admin/sendMessage +GET /pools/admin/getMessages +DELETE /pools/admin/dissociateAndRemove/{id} +``` + +``` +GET /teams/pools/{poolId}/membersAccessGranted +→ {teamMembersAlreadyGrantedPool: [...]} +``` + +--- + +## 5. Mobile API — /api/ Endpoints + +Base URL: `https://qualif.mysolem.com` (same host, different path prefix). +Auth: `Authorization: Bearer {token}` on all requests. + +### 5.1 User + +``` +GET /api/getUser +GET /api/getUserWithHisModules +GET /api/getUserPools +GET /api/getUserWateringModuleGroups → {"wateringModuleGroups": []} +GET /api/getUserAgriculturalModuleGroups → {"agriculturalModuleGroups": []} +GET /api/getCustomers → {"err": {"code": "user_is_not_professional"}} +GET /api/users/{userId}/canopy-sites +GET /api/users/{userId}/access-requests +GET /api/users/{userId}/module-access-requests +``` + +`GET /api/getUserPools` is the only endpoint that exposes the pool record `5de53c1c7ed4c45c2cfe0688` via the API test account — the web platform `/user/pools` returns empty for that account. + +### 5.2 Pool + +``` +GET /api/getPoolStatus?attributesToPopulate[]=modules&poolIdentifier={poolId} +``` +Returns pool status with full modules array. Pool ID for Domicile: `5de53c1c7ed4c45c2cfe0688`. + +Pool config fields: `volume` (25 m³), `installationType`, `filteringType`, `waterTreatmentType`, `phRegulationMode`, `chlorineRegulationMode`, `heatingSystemType`, `coverType`, `cyanuricAcidRate`, `model`, `latitude`, `longitude`, `address`. + +### 5.3 Modules + +``` +GET /api/getModuleWithHisUsers?module={moduleId} +GET /api/getModuleInventory?module={gatewaySerial}&startDate={iso8601}&endDate={iso8601}&data=1 +→ {children: [...]} ← all child modules under this gateway with full config +GET /api/getModuleBackups?module={moduleId} +→ array of saved program snapshots +GET /api/getPrograms?id={moduleId} +→ full program objects including startTimesCycles, stationsDuration +PUT /api/updateModule +Content-Type: application/json +{"module": {"id": "moduleId", "batteryVoltage": 77, "powerState": 0, "status": {...}}} +``` + +### 5.4 Gateway-Routed Module Access + +All real-time commands go through the LRMB10 gateway. URL pattern: +``` +/api/module/{gatewaySerial}/{action}/{childSerial6} +``` +- `gatewaySerial` = full 16-char serial, e.g. `1000000705320001` +- `childSerial6` = **last 6 hex chars** of the child module's serial (case-insensitive) + +**Domicile routing:** + +| Child | childSerial6 | Full serial | +|---|---|---| +| LRPC-F1DA27 | `F1DA27` | `150302F1DA270001` | +| LR6AG-070917 | `070917` | `6606010709170001` | +| LRMS-0721DC | `0721DC` | `7400040721DC0001` | +| LR2IPECO-0C9282 | `0C9282` | `3802020C92820001` | +| LRLEVEL-0D2ED9 | `0D2ED9` | `3A01020D2ED90001` | +| LRPR-0D6800 | `0D6800` | — | + +**Read endpoints:** +``` +GET /api/module/{gatewaySerial}/information ← gateway itself +GET /api/module/{gatewaySerial}/information/{child6} ← child real-time info (LoRa roundtrip) +GET /api/module/{gatewaySerial}/status/{child6} ← current output/input states +GET /api/module/{gatewaySerial}/configuration/{child6} ← device configuration +GET /api/module/{gatewaySerial}/programs/{child6} ← programs stored on device +GET /api/module/{gatewaySerial}/inventory ← all children (= getModuleInventory) +``` + +Gateway information response example: +```json +{ + "mtype": "10", + "msn": "1000000705320001", + "name": "LRMB10-070532 (Perso)", + "vsoft": "6.5.0", + "ble": "1.0.13", + "ssid": "Freebox-2A5E5D", + "bluetoothLink": true, + "wifiLink": true, + "ethernetLink": false, + "modemLink": false, + "up": 782800, + "cdate": "2026-06-05T00:11:38+02:00" +} +``` + +**Write endpoints:** +``` +POST /api/module/{gatewaySerial}/status/{child6} +Content-Type: application/json +{"ag": [{"index": 0, "rainDelay": 0}, {"index": 1, "rainDelay": 0}]} +→ sets rain delay per output; response includes full device status + +POST /api/module/{gatewaySerial}/programs/{child6} +Content-Type: application/json +{"programs": [...], "programmationType": 1} +→ sends programs to device over LoRa + +POST /api/module/{gatewaySerial}/manual/{child6} +Content-Type: application/json +{...command body...} +→ sends a manual command to device over LoRa +``` + +### 5.5 Sensor & Chart Data + +``` +GET /api/getInputs?id={moduleId} +→ all inputs for module with expression, expressionInverse, alert config + +GET /api/getComputedChartData + ?inputId={inputId} + &startDate={iso8601} + &endDate={iso8601} + &numberOfBar={N} + &locale=fr +→ input data bucketed into N bars; includes computedSensorData array + +GET /api/getComputedOutputActivationHistory + ?moduleId={moduleId} + &outputIndex={0|1|2} + &startDate={iso8601} + &endDate={iso8601} + &numberOfBar={N} +→ [{currentPeriodStartDate, currentPeriodEndDate, value}] + value=1 means output was active during that period + numberOfBar=1440 over 24h = 1 bar per minute +``` + +### 5.6 Programs (Mobile) + +``` +GET /api/getPrograms?id={moduleId} +PUT /api/updatePrograms +Content-Type: application/json +{ + "programmationType": 1, + "programs": [{"id": "...", "name": "...", "waterBudget": 100, "windows": [...], "programCharacteristics": {...}}] +} +→ {updatedPrograms: [...]} + +POST /api/library/program-templates +{"limit": 10, "moduleId": "moduleId", "offset": 0} +→ {"programTemplates": []} +``` + +### 5.7 Documents & Metadata + +``` +GET /api/getDocument?documentType=installation-guide&product-type={moduleType} +→ {"url": "https://qualif.mysolem.com/files/notices/{type}/solem/fr-installation-guide.pdf"} + +GET /api/getAdvertisingProfilePresets +→ [{name, ble: {windows: [...]}, loRa: {interval}}] +Profiles: default, ecoMode, etc. + +GET /api/getAllBanishedSerialNumbers +→ ["120C0200750001", ...] +Blacklisted/revoked device serial numbers. +``` + +### 5.8 Reporting (App Telemetry) + +Called by the app after sending data to devices — informational, do not depend on responses. + +``` +POST /api/reportModuleDatasSent {"module": "moduleId"} +POST /api/reportProgramsDatasSent {"programs": ["programId", ...], "module": "moduleId"} +POST /api/reportManualCommandSent {"route": "http", "command": {...}, "id": "moduleId"} +``` + +### 5.9 Push Notifications + +``` +POST /api/addApnsTokenToUser +{"isSandbox": false, "topic": "com.mysolem.qualif", "user": "{userId}", "token": "{apnsToken}"} +→ {"code": "ok"} +``` + +### 5.10 Web vs Mobile API Comparison + +| Feature | Web (cookie) | Mobile (Bearer) | +|---|---|---| +| Auth | Session cookie | OAuth2 Bearer | +| Pool access | Admin-only / main account | `GET /api/getUserPools` | +| Module routing | Direct by ID | Via gateway serial + childSerial6 | +| Sensor data | `getComputedSensorData` (raw ticks) | `getComputedChartData` (bucketed) | +| Output history | `output/events` | `getComputedOutputActivationHistory` | +| Programs | `programs/modules` | `getPrograms` (richer fields) | +| Module inventory | `modules/search` | `getModuleInventory` via gateway | +| Program backups | — | `getModuleBackups` | + +--- + +## 6. Device Control + +All control commands work on both `qualif.mysolem.com` and `qualif.myindygo.com`. All require session cookie. + +### 6.1 Pool Controller (LRPC) — linesControl + +Used for LRPC (`lr-pc`), LRLEVEL (`lr-niv`), and other pool devices. + +``` +POST /remote/module/control +Content-Type: application/json + +{ + "moduleSerialNumber": "150302F1DA270001", + "linesControl": [ + {"index": 0, "action": 1} + ] +} +``` + +`moduleSerialNumber` is the device's `serialNumber` field (not the MongoDB `id`). +Multiple entries in `linesControl` can be sent in one call. + +**Action codes (verified from app mitmproxy captures + live testing):** + +| action | meaning | extra params | +|---|---|---| +| `0` | Stop pump (index 0 only) | — | +| `1` | **Stop / cancel** any running command | — | +| `2` | Timed impulse | `time`: `"MM:SS"` string (e.g. `"30:00"` = 30 min) | +| `3` | Boost filtration | `time`: `"MM:SS"` | +| `4` | Start indefinitely (runs until action 1) | — | +| `5` | Pause N days (blocks manual + scheduled programs) | `manualDuration`: N (integer days) | +| `11` | Backwash cycle | `time`: encoded duration, `speed`: pump speed level | + +**Important notes:** +- Action 0 returns `{"erreurtraitement": 6}` if called on a timed output that's running. +- To stop a running timed output: send `action 1`, or override with `action 2, time: "00:01"`. +- Action 5 `manualDuration` is **days** (not seconds). Response includes `"pause": N` (days remaining). +- **Response shows pre-command device state** — the new command takes effect on the next LoRa downlink window. + +**Response structure (LRPC):** +```json +{ + "dialogTimeStamp": "2026-06-04T22:35:16Z", + "ackTimestamp": 1780612516, + "temperature": 19, + "pool": [ + {"index": 0, "time": "00:00", "tempRef": 7, "value": 0}, + {"index": 1, "origin": 2, "time": "30:00", "value": 1, "info": ["man"]}, + {"index": 2, "time": "00:00", "tempRef": 7, "value": 0} + ], + "sensorState": [ + {"index": 0, "value": 2318}, + {"index": 1, "value": 2147483647} + ], + "inputsAlerts": [], + "radio": ["cmd"] +} +``` + +`pool[n].value`: 1 = running, 0 = stopped. +`pool[n].origin`: 2 = manual, 3 = boost. +`pool[n].info`: `["man"]` = manual, `["boost"]` = boost mode. +`pool[n].pause`: days remaining on a pause (only present when paused). +`radio: ["cmd"]` = command queued for LoRa transmission. + +**Common examples:** + +```bash +# Start pool light (index 1) for 30 minutes +curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \ + -H "Content-Type: application/json" \ + -d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":2,"time":"30:00"}]}' + +# Stop pool light +curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \ + -H "Content-Type: application/json" \ + -d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":1}]}' + +# Boost filtration for 2 hours +curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \ + -H "Content-Type: application/json" \ + -d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":0,"action":3,"time":"02:00"}]}' + +# Pause pool light for 3 days (disables manual + programs) +curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \ + -H "Content-Type: application/json" \ + -d '{"moduleSerialNumber":"150302F1DA270001","linesControl":[{"index":1,"action":5,"manualDuration":3}]}' + +# Stop fill valve (LRLEVEL, index 0) +curl -b cookies.txt -X POST https://qualif.myindygo.com/remote/module/control \ + -H "Content-Type: application/json" \ + -d '{"moduleSerialNumber":"3A01020D2ED90001","linesControl":[{"action":0,"index":0}]}' +``` + +### 6.2 Irrigation Valve (LR6AG) — sendManualModuleCommand + +Used for `lr-ag`, `lr-ip-eco`, and other irrigation modules. + +``` +POST /module/sendManualModuleCommand +Content-Type: application/x-www-form-urlencoded +``` + +All patterns confirmed via mitmproxy captures. + +| Intent | Form params | +|---|---| +| Run station N minutes | `commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=N` | +| Run all stations N minutes | `commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=N` | +| Cycling station (on/off intervals) | `commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0&commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId}` | +| Stop specific station | `commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId}` | +| Stop everything | `commandType=manualStop` | +| Enable (no rain delay) | `commandType=status&command=on&commandParams=0` | +| Disable / rain delay N days | `commandType=status&command=off&commandParams=N` | +| Max rain delay | `commandType=status&command=off&commandParams=255` | +| Run program | `commandType=program&command={programId}` | + +**Notes:** +- `command` accepts an output ID string, `"allStations"`, or a program ID. +- `agriculturalModuleId` is required for stop and cycling station commands. +- `commandType=manualStop` takes no `outputIndex` or `commandParams`. + +**JSON body variant (also accepted):** +```json +{ + "moduleSerialNumber": "6606010709170001", + "commandType": "station", + "outputIndex": 0, + "command": null, + "commandParams": {"hours": 0, "minutes": 15, "on": 0, "off": 0} +} +``` + +Rain delay example (suspend station 0 indefinitely): +```json +{ + "moduleSerialNumber": "6606010709170001", + "commandType": "status", + "command": "off", + "commandParams": 255, + "outputIndex": 0 +} +``` + +### 6.3 Flush Commands to Device + +Triggers the platform to push all queued commands to a site's modules immediately: + +``` +POST /canopy/{siteId}/sendModulesCommand +Content-Type: application/json +{} +``` +Response: `202 Accepted` — `{ "message": "Modules command accepted for processing" }` + +Use after `PUT /programs` to force immediate LoRa push. + +### 6.4 LoRa Data Push (Force Sync) + +``` +POST /modules/sendDataViaLoRaWAN +POST /agriculturalModuleGroup/sendDataViaLoRaWAN +POST /wateringModuleGroup/sendDataViaLoRaWAN +``` + +### 6.5 Module Rename + +``` +GET /remote/module/name?moduleId={moduleId}&name={newName} +``` + +--- + +## 7. MyIndygo — Pool Platform + +MyIndygo (`https://qualif.myindygo.com`) is the pool-focused frontend on the same backend. All endpoints from section 4 work identically. + +### 7.1 Pool Auth Note + +The test API account (`arnaud.nelissen.api@icloud.com`) has different user IDs on each platform: +- MySOLEM: `5de53bca7ed4c45c2cfe067f` +- MyIndygo: `6a21c3db5ebe0fa5f7885460` + +The pool ID `5de53c1c7ed4c45c2cfe0688` is accessible via the mobile API (`GET /api/getUserPools`) or when logged in as the main account (`anelissen@solem.fr`) on the web. + +### 7.2 LRPC Filtration Schedule (Domicile) + +Current program 0 (filtration, type 4), runs every day: + +| Window | Start | End | Duration | +|---|---|---|---| +| Morning | 05:00 | 06:00 | 1h | +| Midday | 12:00 | 14:00 | 2h | +| Evening | 22:00 | 23:00 | 1h | +| **Total** | | | **4h/day** | + +Special features: +- **Micro-filtration** (frost mode): activates every 480 min for 10 min when temp < 5°C, speed 1 +- **Temperature-adaptive**: 2 schedules (96d + 31d/year) with 12 temperature thresholds adjusting run windows + +Program 1 (auxiliary, type 2): 21:30–22:30 active window, 5-min cycles — likely electrolyser or secondary treatment pump. + +### 7.3 Pool Sensor Quick Reference + +| Sensor | Input ID | Unit | Notes | +|---|---|---|---| +| Water temp | `6480da07c2c0896da120eccc` | °C (raw ÷ 100) | Healthy: 20–28°C | +| pH | `6480da07c2c0896da120eccd` | pH (raw ÷ 100) | Target: 7.2–7.6 | +| ORP/Redox | `6480da07c2c0896da120ecce` | mV (raw) | Healthy: 650–750 mV | +| Filter pressure | `6a17218be0b88ea3e9255251` | bar: `(1.5×raw−500)/1000` | Running: ~0.70–0.75 bar; off: ~0.08 bar | +| Pool level switch | `6a174dd1e0b88ea3e925531e` | 0/1 | 0 = fill needed, 1 = OK | +| Fill volume | `6a1c4b153d6c33ab843edc15` | L | Volume added per fill cycle | +| Fill valve state | `6a1c4b153d6c33ab843edc16` | 0/1 | 1 = valve closed | + +### 7.4 Fill System Logic + +``` +pool-level-sensor (float switch) reads 0 (low) + → LRLEVEL (lr-niv) opens fill valve (output 0, Station 1) + → edc15 input measures litres added + → valve closes when pool-level-sensor returns 1 (OK) +``` + +Filter pressure (`LRPR`) can be used to detect filtration start/stop and dirty filter (pressure rises above ~0.80 bar when filter needs backwash). Backwash reminder fires every ~15 days. + +### 7.5 MyIndygo Feature Flags + +Pool-specific flags returned by `GET /features` on `qualif.myindygo.com`: + +| Flag | Meaning | +|---|---| +| `ocedisWaterChemistryCorrection` | OCEDIS chemical dosing correction | +| `enableBSTSubscriptions` | BST subscription management | +| `enablePolytropicAPI` | Variable-speed pump (Polytropic) integration | +| `enableGraphicalProgrammation` | Visual program schedule editor | +| `enableNdviSatellite` | NDVI satellite imagery | +| `enableEvapotranspiration` | ET-based automated watering | +| `clusterTurfOpened` | Turf cluster watering mode | + +--- + +## 8. Device Reference — Domicile + +### 8.1 LRMB10-070532 — LoRa Gateway + +**ID**: `60ca4276ad69dac1558f7c61` | **Serial**: `1000000705320001` +All other modules communicate through this gateway via LoRa radio. + +| Field | Value | +|---|---| +| Firmware | 6.5.0 | +| BLE | 1.0.13 | +| WiFi MAC | `10:52:1C:01:2D:F4` | +| BT MAC | `C8:B9:61:07:05:32` | +| WiFi SSID | `Freebox-2A5E5D` | +| Links | WiFi ✓, Bluetooth ✓, Ethernet ✗, Modem ✗ | + +`remote/module/configuration` works directly (gateway is WiFi-connected, not LoRa-only). + +### 8.2 LRPC-F1DA27 — Pool Controller + +**ID**: `6481bcea8782b78554c02bed` | **Serial**: `150302F1DA270001` | **Type**: `lr-pc` +Controls filtration pump (index 0), pool light (index 1), and a third output (index 2, unused). + +Pool config: 25 m³, rectangular 6×3 m depth 1.4 m, outdoor, glass filter, stabilised chlorine tablets, automatic pH regulation, manual chlorine regulation. + +Programs: see section 7.2. +Alert history: `GET /maintenance-logs-data?moduleId=6481bcea8782b78554c02bed` +`remote/module/configuration` returns `module_not_reachable` — LoRa relay only. + +### 8.3 LRPS-0565C0 — Pool Analyser + +**ID**: `6480da07c2c0896da120ecca` | **Serial**: `9400030565C00001` | **Type**: `lr-mas` +Measures pool water temperature, pH, and ORP/redox. Battery at 60% — alerts fired Feb 2026. Calibration needed (flagged Mar 2026). + +| Input | ID | Type | Interval | +|---|---|---|---| +| Water temperature | `6480da07c2c0896da120eccc` | 5 | ~30 min | +| pH | `6480da07c2c0896da120eccd` | 6 | varies | +| ORP/Redox | `6480da07c2c0896da120ecce` | 7 | varies | + +No outputs — pure analyser. + +### 8.4 LR6AG-070917 — Irrigation Valve Controller + +**ID**: `626984f038ec558c598bb6d5` | **Serial**: `6606010709170001` | **Type**: `lr-ag` +6 irrigation stations, 1 rain sensor input. Battery: 81%. Relay: LRMB10. + +All 6 programs reset on 2026-06-04 with no active windows. +Stations 0 and 1 currently have 255-day rain delay (suspended indefinitely). + +| Station | Output ID | Index | Flow rate | +|---|---|---|---| +| Station 1 | `626984f038ec558c598bb6d8` | 0 | 30 m³/h | +| Station 2 | `626984f038ec558c598bb6d9` | 1 | 14 m³/h | +| Station 3 | `626984f038ec558c598bb6da` | 2 | 20 m³/h | +| Station 4 | `626984f038ec558c598bb6db` | 3 | 1 m³/h | +| Station 5 | `626984f038ec558c598bb6dc` | 4 | 38 L/h | +| Station 6 | `626984f038ec558c598bb6dd` | 5 | 38 L/h | + +Rain sensor (input `626984f038ec558c598bb6d7`, type 2): 1 = rain, 0 = clear. + +### 8.5 LRMS-0721DC — Sensor Station + +**ID**: `60ca42adad69dac1558f7c64` | **Serial**: `7400040721DC0001` | **Type**: `lr-ms` +Pure sensor — no outputs, no programs. Battery: 78%. `relayIsLongPolling: true`. + +| Input | ID | Type | Interval | +|---|---|---|---| +| Plumbago (soil moisture) | `60ca42adad69dac1558f7c66` | 12 | 60 min | +| Pelouse (soil moisture) | `60ca42adad69dac1558f7c67` | 12 | 60 min | +| Air/soil temperature | `60ca42adad69dac1558f7c69` | 5 | 15 min | + +Moisture spikes (>10% jump within an hour) correlate with irrigation events — useful to infer watering history when `output/events` returns no records. + +### 8.6 LR2IPECO-0C9282 — IP ECO Controller + +**ID**: `6516ca57b7c098e6807cb332` | **Serial**: `3802020C92820001` | **Type**: `lr-ip-eco` +2 irrigation stations + pulse flow meter. Battery: 89% primary, secondary battery: 3200 mV. All 3 programs have no active windows. + +| Input | ID | Type | +|---|---|---| +| Flow meter | `6516ca57b7c098e6807cb334` | 33 (flow, L) | + +Flow meter alternates: values are cumulative per minute, pattern suggests short irrigation bursts. +`watering.state: 1` in remote state = currently active at last poll. + +### 8.7 LRPR-0D6800 — Filter Pressure Sensor + +**ID**: `6a17218be0b88ea3e925524f` | **childSerial6**: `0D6800` | **Type**: `lr-pr` +Pure pressure sensor. Battery: 94%. 1-minute reporting interval. + +Input `6a17218be0b88ea3e9255251`, type 8. Expression: `(1.5 × raw − 500) / 1000` → bar. + +Normal operating pressure: **0.70–0.75 bar** (pump running). Drop to ~0.08 bar = pump stopped. +Rising pressure above ~0.80 bar = dirty filter — backwash needed. + +### 8.8 pool-level-sensor — Float Level Switch + +**ID**: `6a174dd1e0b88ea3e925531c` | **Type**: `pool-level-sensor` +Binary float switch, only transmits on state change (~11 events per month). Battery: **41% — needs replacement soon**. Cleaning reminder fired 2026-05-28. + +Input `6a174dd1e0b88ea3e925531e`, type 21. Raw passthrough: 0 = below threshold (fill needed), 1 = OK. + +### 8.9 LRLEVEL-0D2ED9 — Pool Fill Valve Controller + +**ID**: `6a1c4b153d6c33ab843edc13` | **Serial**: `3A01020D2ED90001` | **Type**: `lr-niv` +Controls pool fill valve automatically based on pool-level-sensor. Battery: 94%. + +| Input | ID | Type | Notes | +|---|---|---|---| +| Fill volume | `6a1c4b153d6c33ab843edc15` | 33 | L added per fill cycle; polynomial expression | +| Level state | `6a1c4b153d6c33ab843edc16` | 149 | 1 = valve closed, 0 = open | + +Output `6a1c4b153d6c33ab843edc17` (Station 1, index 0) — fill valve. No scheduled program; triggered by level sensor. + +To query fill history: `GET /remote/input/getComputedSensorData?inputId=6a1c4b153d6c33ab843edc15` over wide date range, filter ticks where `value > 0`. + +--- + +## 9. Write Endpoints Reference + +All mutation endpoints grouped by risk. All require session cookie unless noted. + +### 9.1 Device Control (high impact — sends LoRa radio commands) + +``` +POST /remote/module/control pool controller linesControl (see section 6.1) +POST /module/sendManualModuleCommand irrigation valve commands (see section 6.2) +POST /canopy/{siteId}/sendModulesCommand flush commands to device +POST /modules/sendDataViaLoRaWAN +POST /agriculturalModuleGroup/sendDataViaLoRaWAN +POST /wateringModuleGroup/sendDataViaLoRaWAN +``` + +### 9.2 Programs (persistent — survives device reboot) + +``` +PUT /programs {"programs": [...], "module": "moduleId"} +POST /programs create new program +DELETE /programs +PATCH /programs/name +POST /program/update socket.io variant (immediate push to device) +PUT /program/update REST variant +POST /checkAvailableWindows check time conflicts before saving +PUT /api/updatePrograms mobile API variant +``` + +### 9.3 Module Configuration + +``` +POST /remote/module/configuration +POST /remote/module/configuration/and/programs +POST /input/update update input settings (name, thresholds, alerts) +PUT /output/update update output settings (name, flow rate) +GET /remote/module/name?moduleId=...&name=... rename via LoRa +PUT /module/update +PUT /module/update/address +PUT /module/update/status +POST /module/update/name +POST /module/update/securityMode +POST /remote/module/advertisingProfile +POST /remote/module/dissociate {"relayMsn": "...", "modulesMsn": ["..."]} +PUT /module/dissociate/{moduleId} +``` + +### 9.4 Site / Pool Management + +``` +POST /canopy-sites +PUT /pools/update/address +POST /pools/admin/sendMessage +POST /wateringModuleGroup/create +PUT /wateringModuleGroup/update +DELETE /wateringModuleGroup/delete +POST /addAgriculturalGroupToUser +PUT /teams/handleAccessGrantedPools +POST /teams/members/add +``` + +### 9.5 Journal / Notifications + +``` +PUT /journal/acknowledge/all +PUT /journal/acknowledge/records +POST /canopy-sites/{siteId}/notifications/acknowledge-all +POST /journal/warnings/accepted +``` + +### 9.6 Socket.io Write Calls + +These bypass the normal HTTP layer — not captured by Safari recordings: + +``` +socket.post /checkAvailableWindows +socket.post /input/update +socket.post /program/update +socket.post /remote/module/configuration +socket.post /remote/module/configuration/and/programs +socket.post /remote/module/name +socket.put /module/update +socket.put /module/update/address +socket.put /pools/update/address +socket.put /program/update +``` + +--- + +## 10. Reference Tables + +### Module Types + +| Type string | Device name | Description | +|---|---|---| +| `lr-mb-10` | LRMB10 | LoRa gateway v10 | +| `lr-mb-30` | LRMB30 | LoRa gateway v30 | +| `lr-mb-100` | LRMB100 | LoRa gateway v100 | +| `lr-ms` | LRMS | LoRa sensor station | +| `lr-ms-eco` | LRMS_ECO | Eco sensor station | +| `lr-ms-v2` | LRMS v2 | Sensor station v2 | +| `lr-ms-rs485` | LRMSRS485 | RS485 sensor station | +| `lr-ag` | LR6AG | LoRa agricultural valve (6 stations) | +| `lr-mas` | LRPS | LoRa master valve / pool analyser | +| `lr-pc` | LRPC | LoRa pool controller | +| `lr-pc-vs` | LRPCVS | Pool controller with variable-speed pump | +| `lr-ip-eco` | LR2IPECO | LoRa IP ECO (2-station + flow meter) | +| `lr-ip-fl` | LR1IPFL | IP flow metering | +| `lr-ip-fl-v2` | LR6IP | IP flow v2 | +| `lr-ip-wan-v2` | LRIP6 WAN | IP WAN v2 | +| `lr-is` | LR6IS/LR12IS | LoRa input sensor (multi-input) | +| `lr-is-city` | LR6IS | City variant input sensor | +| `lr-niv` | LRLEVEL | LoRa level controller (fill valve) | +| `lr-pr` | LRPR | LoRa pressure sensor | +| `lr-mv` | LRMV/LRPM1 | LoRa motor valve | +| `lr-ol` | LROL4 | LoRa OL (lighting control) | +| `lr-bst-100` | LRBST100 | BST 100 booster | +| `lr-bst-react-4g` | LR-BST R4G | BST 4G reactive booster | +| `pool-level-sensor` | POOL-LVL | Pool float level switch | +| `wf-mb` | WiFi gateway | WiFi gateway | +| `wf-is` / `bl-is` | WiFi/BT sensor | WiFi or Bluetooth sensor | +| `bl-ip` / `bl-ip-v2` | BL6IP | Bluetooth IP | +| `wf-ol` | WiFi OL | WiFi online | +| `joro` / `joro-v2` | WB-xxxx | Joro water balance meter | +| `smart-is` | SmartIS | Smart input sensor | +| `smart-is-lcd` | VILLA-/SMT-HOME | Smart IS with LCD | +| `ipx` | GEN_xxxx | IPX controller | + +### Input Type Codes + +| Code | Meaning | Unit | Notes | +|---|---|---|---| +| `2` | TOR (on/off) | — | Rain sensor: 1=rain, 0=clear | +| `5` | Air/soil temperature | °C | LRMS ambient sensor | +| `6` | pH | — | Raw ÷ 100 = pH units | +| `7` | ORP / Redox | mV | Raw = mV | +| `8` | Pressure | bar | Expression: `(1.5×raw−500)/1000` | +| `12` | Soil moisture | % (unit `10`) | LRMS capacitive probe | +| `20` | Air moisture | — | | +| `21` | Pool level TOR switch | 0/1 | 0=low/fill needed, 1=OK | +| `33` | Volumetric / flow | L (unit `1`) | Cumulative, pulse-based | +| `133` | Water temperature | °C (unit `2`) | LRPC/LRPS probe | +| `149` | Fill valve / level state | 0/1 | LRLEVEL: 1=closed | + +### linesControl Action Codes (LRPC) + +| action | meaning | params | verified | +|---|---|---|---| +| `0` | Stop pump (index 0 only) | — | Yes (LRLEVEL) | +| `1` | Stop / cancel any running command | — | Yes (app capture) | +| `2` | Timed impulse | `time: "MM:SS"` | Yes (app capture + live) | +| `3` | Boost filtration | `time: "MM:SS"` | Yes (Safari capture) | +| `4` | Start indefinitely | — | Yes (notebook pattern) | +| `5` | Pause N days | `manualDuration: N` (days) | Yes (app capture) | +| `11` | Backwash cycle | `time`, `speed` | Partial (JS analysis) | + +--- + +## 11. Security Findings + +Assessment from black-box analysis. All findings verified on `qualif.mysolem.com`. Reported for awareness; not exploited beyond authorized testing scope. + +### Critical + +| # | Endpoint | Finding | +|---|---|---| +| 1 | `POST /modules/search` | Empty query returns **4,607 modules** with name, serial, MAC, firmware, GPS — no ownership enforcement (BOLA) | +| 2 | `POST /users/search` | Returns full user documents including **password hashes** (unsalted SHA-256), **active reset tokens** (direct account takeover), APNs/FCM push tokens — BOLA | +| 3 | `POST /outputs/search` + `POST /programs/search` | Empty query returns 12,034 outputs and 6,073 programs platform-wide — no ownership enforcement | +| 4 | `GET /remote/module/configuration` | Returns WiFi SSID, MACs, firmware for **any module ID** — no ownership check | +| 5 | `GET /remote/input/getComputedSensorData` | Any `inputId` accepted — full sensor history exfiltration across tenants | +| 6 | `POST /module/sendManualModuleCommand` | Takes `moduleSerialNumber` (obtainable from #1) — likely no ownership check, unconfirmed | + +### High + +| # | Finding | +|---|---| +| 7 | Google Maps API key `AIzaSyDh8fCC9vTEuN9eKewxgqWXGP9ENb5an1c` hard-coded in HTML, no referrer restriction | +| 8 | Session cookie missing `SameSite` attribute → CSRF possible on state-changing endpoints | +| 9 | No rate limiting on `POST /login` — credential stuffing uninhibited | +| 10 | MongoDB ObjectIds are sequentially guessable (timestamp + machine + counter), accelerating enumeration | + +### Medium / Low + +| # | Finding | +|---|---| +| 11 | `Access-Control-Allow-Origin: ` (empty string) — CORS misconfiguration | +| 12 | No `Content-Security-Policy` header on React frontend | +| 13 | `X-Powered-By: Sails ` on every response — unnecessary fingerprinting | +| 14 | Qualification environment appears to use live production data (real GPS, real addresses, live telemetry) | +| 15 | Input IDs embedded as `data-input-id` HTML attributes — completes enumeration chain without guessing | diff --git a/api-docs/control.md b/api-docs/control.md new file mode 100644 index 0000000..3b86bb1 --- /dev/null +++ b/api-docs/control.md @@ -0,0 +1,258 @@ +# MySolem / MyIndygo API — Device Control + +All commands work on both `https://qualif.mysolem.com` and `https://qualif.myindygo.com`. Requires session cookie. + +--- + +## Manual irrigation command + +``` +POST /module/sendManualModuleCommand +Content-Type: application/x-www-form-urlencoded +``` + +Payloads are **form-encoded**. All patterns confirmed via mitmproxy captures. A JSON body variant (same fields) also works — see the example at the end of this section. + +Note: the `command` field is not an action name — it accepts an **output ID string**, `"allStations"`, or a **program ID**. + +### Status commands (enable / disable module) + +``` +commandType=status&command=on&commandParams=0 → enable (no rain delay) +commandType=status&command=off&commandParams=0 → disable +commandType=status&command=off&commandParams=1 → 1-day rain delay +commandType=status&command=off&commandParams=255 → disable with max rain delay +``` + +### Station commands + +Run a specific station for a duration: +``` +commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=1 +``` + +Cycling station (on/off intervals + optional agricultural module): +``` +commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0 + &commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId} +``` + +Stop a specific station: +``` +commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId} +``` + +Run all stations for a duration: +``` +commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=2 +``` + +Stop everything immediately (no outputIndex, no commandParams): +``` +commandType=manualStop +``` + +### Program command (NEW) + +Send a specific program to the module: +``` +commandType=program&command={programId} +``` + +### JSON body equivalent (original documented form) + +```json +{ + "moduleSerialNumber": "6606010709170001", + "commandType": "station", + "outputIndex": 0, + "command": null, + "commandParams": { + "hours": 0, + "minutes": 15, + "on": 0, + "off": 0 + } +} +``` + +- `outputIndex`: 0-based index of the output/station +- `hours` + `minutes`: duration to water +- `on` / `off`: cycle timing (0 = continuous) +- `agriculturalModuleId`: required when stopping or cycling a specific station + +--- + +## Flush pending commands to device + +Triggers the platform to push all queued commands to a site's modules: + +``` +POST /canopy/{siteId}/sendModulesCommand +Content-Type: application/json + +{} +``` + +Response: `202 Accepted` — `{ "message": "Modules command accepted for processing" }` + +--- + +## Remote module rename + +``` +GET /remote/module/name?moduleId={moduleId}&name={newName} +``` + +--- + +## Dissociate module from relay + +``` +POST /remote/module/dissociate +Content-Type: application/json + +{ + "relayMsn": "relaySerialNumber", + "modulesMsn": ["childSerialNumber"] +} +``` + +--- + +## Update module metadata + +``` +PUT /module/update +Content-Type: application/json + +{ "module": { "id": "moduleId", "name": "New Name", ... } } +``` + +``` +PUT /module/update/address +PUT /module/update/status +``` + +--- + +## Remove module + +``` +DELETE /module/{moduleId} +GET /module/remove/from/my/modules/{moduleId} +``` + +--- + +## Program update (socket.io legacy) + +```javascript +io.socket.post("/program/update", { + programs: [...], + module: moduleId, + programmationtype: "temporal" +}, callback) +``` + +--- + +## Pool Controller Control (LRPC / lr-pc) — MyIndygo + +Pool controllers use a **different control endpoint** with a `linesControl` array structure. + +``` +POST /remote/module/control +Content-Type: application/x-www-form-urlencoded +``` + +Payload (JSON body — `Content-Type: application/json`): +```json +{ + "moduleSerialNumber": "150302F1DA270001", + "linesControl": [ + { + "index": 0, + "action": 2, + "time": "60:00" + } + ] +} +``` + +**Action codes (verified from app mitmproxy captures + live testing):** + +| action | meaning | extra params | notes | +|---|---|---|---| +| `0` | Stop pump (index 0 only) | — | Returns `erreurtraitement: 6` on other outputs | +| `1` | **Stop / cancel** any running command | — | Works on all outputs; cancels both timed and indefinite runs | +| `2` | Timed impulse | `time`: `"MM:SS"` string | Also used to override a running timer early | +| `3` | Boost filtration | `time`: `"MM:SS"` | Response: `origin:3, info:["boost"]` | +| `4` | Start indefinitely | — | Runs until action 1 is sent; common in notebook for evening light use | +| `5` | Pause N days | `manualDuration`: N (integer days) | Disables output including scheduled programs; response shows `"pause": N` = days remaining | +| `11` | Backwash cycle | `time`: encoded duration, `speed`: pump speed level | | + +**To stop a timed impulse early**: send `action 1` — cancels any running manual command. +**To stop a timed output (alternative)**: override with `action 2, time: "00:01"`. + +**Pause semantics**: action 5 suspends the output entirely (manual commands and scheduled programs) for N days. The response pool state includes `"pause": N` showing days remaining. `manualDuration` here is **days**, not seconds. + +**Response timing**: the pool state in the response always reflects the **pre-command device state** — it shows what the device reported before processing the new command. The command takes effect on the next LoRa downlink window. + +Duration format: `"MM:SS"` string, e.g. `"30:00"` = 30 minutes. Verified from real capture. + +**Examples:** + +Stop fill valve (LRLEVEL): +```json +{"moduleSerialNumber": "3A01020D2ED90001", "linesControl": [{"action": 0, "index": 0}]} +``` + +Boost filtration 1 min (LRPC): +```json +{"moduleSerialNumber": "150302F1DA270001", "linesControl": [{"action": 3, "time": "01:00", "index": 0}]} +``` + +**Response** — full real-time device state, e.g. for LRPC after boost: +```json +{ + "dialogTimeStamp": "...", + "ackTimestamp": 1780609493, + "temperature": 20, + "pool": [ + {"index": 0, "origin": 3, "time": "01:00", "value": 1, "info": ["boost"]}, + {"index": 1, "time": "00:00", "tempRef": 7, "value": 0} + ] +} +``` + +**Python example — timed pool pump impulse (30 min on Station 2):** +```python +import json +s.post(f"{BASE}/remote/module/control", + json={ + "moduleSerialNumber": "150302F1DA270001", + "linesControl": [{"index": 1, "action": 2, "time": "30:00"}] + } +) +``` + +--- + +## LRPC output IDs (Domicile) + +| Index | Name | Output ID | +|---|---|---| +| 0 | Station 1 | `6481bcea8782b78554c02bf1` | +| 1 | Station 2 | `6481bcea8782b78554c02bf2` | +| 2 | Station 3 | `6481bcea8782b78554c02bf3` | + +--- + +## Notes + +- Commands go through the platform's relay infrastructure — the device doesn't need to be directly reachable +- Use `POST /canopy/{siteId}/sendModulesCommand` after updating programs via `PUT /programs` to force an immediate push +- `moduleSerialNumber` is the `serialNumber` field on the module object (not the `id`) +- For irrigation valves (lr-ag, lr-ip-eco): use `POST /module/sendManualModuleCommand` +- For pool controllers (lr-pc, lr-pc-vs): use `POST /remote/module/control` with `linesControl` diff --git a/api-docs/endpoints.md b/api-docs/endpoints.md new file mode 100644 index 0000000..6035d96 --- /dev/null +++ b/api-docs/endpoints.md @@ -0,0 +1,485 @@ +# MySolem API — Endpoints + +All paths relative to `https://qualif.mysolem.com`. All require session cookie unless noted. + +> **Mobile API**: the iOS apps use a separate `/api/` prefix layer with OAuth2 Bearer auth. See `api-docs/mobile-api.md`. + +--- + +## Feature flags + +``` +GET /features +``` +Returns flat JSON object of boolean flags (`dashboard`, `enableHydraulicSupervision`, …). + +--- + +## Sites + +``` +GET /canopy-sites?userId={userId} +``` +Returns `{ items: [...], total: N }`. Each item includes full `modules` array and `fencing` config. + +``` +GET /canopy-sites/{siteId}/users +GET /canopy-sites/{siteId}/modules/indicators +GET /canopy-sites/{siteId}/informations +GET /canopy-sites/{siteId}/rules +GET /canopy-sites/{siteId}/map/modules +GET /canopy-sites/{siteId}/modules/eco-mode-compatible +GET /canopy-sites/{siteId}/site-notes +GET /canopy-sites/{siteId}/notifications +GET /canopy/{siteId}/waterBudget +``` + +--- + +## Modules + +**Search / list:** +``` +POST /modules/search +Content-Type: application/json + +{ + "query": { "id": { "$in": ["moduleId1", "moduleId2"] } }, + "projection": { "name": 1, "type": 1, "serialNumber": 1 } +} +``` +Response: `{ result: { results: [...], totalCount: null } }` + +``` +GET /modules/available?userId={userId} +``` + +**Paginated modules for a site:** +``` +GET /canopy-sites/{siteId}/modules?limit=20&skip=0&includeTotalCount=true +``` +Response: `{items: [...], total: N}`. Note: items use `_id` (MongoDB style), not `id`. + +**Dashboard chart data:** +``` +GET /modules/echarts-data?includeCustomers=true&includeAgency=false +→ {chartModuleTypes: [{name, value}], chartModuleConnectivities: [...]} +``` + +**SIM card payment check:** +``` +GET /sim-cards/needsPayment?moduleIds={id1}&moduleIds={id2} +→ {result: {"moduleId": false, ...}} +``` + +**Remote state:** +``` +GET /remote/module/state?moduleId={moduleId} +GET /remote/module/configuration?moduleId={moduleId} +``` + +**Module data model (key fields):** +```json +{ + "id": "...", + "name": "...", + "type": "lr-ag", + "serialNumber": "6606010709170001", + "macAddress": "C8:B9:61:...", + "softwareVersion": "7.2.0", + "batteryVoltage": 81, + "battery": 5, + "rssi": 1, + "isOnline": true, + "relay": "relayModuleId", + "latitude": 43.79417, + "longitude": 4.09520, + "lastRadioCommunication": "2026-06-04T17:59:29.000Z" +} +``` + +--- + +## Sensor / Input data + +**Batch input config (most efficient way to get all input metadata):** +``` +GET /inputs?inputId=id1%3Bid2%3Bid3 +``` +Semicolons (`%3B`) separate multiple IDs. Returns array of full input objects with `expression`, `type`, `unit`, `module`, `index`. Much lighter than querying each individually. + +> **Note**: sensor data can be queried per-input or per-module (all inputs at once). + +**All inputs for a module — preferred for dashboard-style loads:** +``` +GET /remote/module/getComputedSensorsData + ?moduleId={moduleId} + &startDate={iso8601} + &endDate={iso8601} + &forceRawOrComputed=computed +``` +Returns an **array** of input objects, each with a `computedSensorData` field containing ticks. +More efficient than querying each input individually. Pass `forceRawOrComputed=raw` for raw ADC values. + +**Single input telemetry:** +``` +GET /remote/input/getComputedSensorData + ?inputId={inputId} + &startDate={iso8601} + &endDate={iso8601} + &ensurePrevTickForContinuity=true +``` + +Response: +```json +{ + "id": "60ca42adad69dac1558f7c66", + "name": "Plumbago", + "module": "60ca42adad69dac1558f7c64", + "type": 12, + "unit": 10, + "interval": 60, + "typeIsMoistureSensor": true, + "computedSensorData": [ + { "tickTimestamp": "2026-06-01T00:42:00.000Z", "value": 54.1875 } + ] +} +``` + +**Last tick only (no date range — fastest for live dashboards):** +``` +GET /input/getLastTickFromInput?inputId={inputId} +``` +Returns `{ "tick": { "input": "...", "value": , "timestamp": "...", "id": "..." } }`. +Value is **raw** (before expression). Divide by 100 for pH/temp from LRPS. + +**Averages:** +``` +GET /inputs/average?inputIds={id1},{id2}&startDate={iso8601}&endDate={iso8601} +``` + +**Update input config:** +``` +POST /input/update +``` + +**CSV export:** +``` +GET /input/getComputedInputIpxDataBetweenDatesAsCsv + ?inputId={inputId}&startDate={iso8601}&endDate={iso8601} +``` + +**Legacy socket.io call** (same route, used from browser JS): +``` +socket.request({ method: "GET", url: "/remote/module/getComputedSensorsData", + data: { moduleId, startDate, endDate } }, callback) +``` + +--- + +## Outputs + +``` +GET /outputs/modules?moduleIds={moduleId} +POST /outputs/search { "query": { "module": { "$in": ["moduleId"] } } } +POST /output/update +``` +Response: `{ outputs: [{ id, module, index, name, flowRate: { value, unit } }] }` +Flow rate unit: `1` = L/h, `2` = m³/h. + +**Output event history:** +``` +GET /output/events?moduleId={moduleId} +GET /output/events?outputId={outputId}&startDate={iso8601}&endDate={iso8601} +``` + +**Watering forecast:** +``` +GET /output/forecast?moduleId={moduleId} +GET /output/forecast?outputId={outputId} +``` + +--- + +## Programs + +``` +GET /programs/modules?moduleIds={moduleId} +PUT /programs + { "programs": [...], "module": "moduleId" } +GET /programs/reset?moduleId={moduleId} +POST /programs/search + { "query": { "id": { "$in": [...] } } } +``` + +Program object: +```json +{ + "id": "...", + "name": "Pelouse Terrasse", + "module": "...", + "index": 0, + "programmingType": "temporal", + "weekDays": 127, + "waterBudget": 100, + "windows": [{ "start": 420, "end": 422, "on": 120, "off": 0 }] +} +``` + +--- + +## Daily watering + +``` +GET /garden/dailyData/{moduleId} +GET /modules/heatmap-data?moduleIds={ids}&startDate=X&endDate=Y +``` + +--- + +## Users + +``` +GET /users-info/{userId}?includeUserGroups=true +GET /users/{userId}/modules +GET /users/{userId}/sites +GET /users/{userId}/module-groups +GET /users/{userId}/subscriptionModules +POST /users/search + { "query": {...}, "projection": {...} } +``` + +**Flexible module list (POST — supports field projection + filtering):** +``` +POST /users/{userId}/modules +Content-Type: application/json + +{ + "fields": {"name": 1, "type": 1, "serialNumber": 1, "displayType": 1}, + "filters": {}, + "addOwnershipFlag": true, + "addOwnerIds": false, + "includeCustomers": true, + "includeAgency": false, + "includeTotalCount": true, + "limit": 20, + "skip": 0 +} +``` +Response: `{modules: [...], totalCount: N}`. +Supported filter fields: `search`, `tagIds`, `displayTypes`, `connectivities`, `powerTypes`. + +**Similar pattern for grouped resources:** +``` +POST /users/{userId}/module-groups → {moduleGroups, totalCount} +POST /users/{userId}/agricultural-module-groups → {agriculturalModuleGroups, totalCount} +POST /users/{userId}/watering-module-groups → {wateringModuleGroups, totalCount} +POST /users/{userId}/customers → {customers, totalCount} +POST /users/{userId}/associated → {users, totalCount} (associated users) +POST /users/{userId}/tags → {tags} +``` + +**Activity log:** +``` +POST /users/{userId}/records +{ + "limit": 10, "skip": 0, + "filters": {"startDate": "2026-05-28T21:51:48.266Z", "showAllEvents": true}, + "includeTotalCount": true, + "includeCustomers": true, + "sortColumn": "timestamp", "sortOrder": -1, + "fields": {"category": 1, "label": 1, "module": 1, "timestamp": 1} +} +→ {records: [...], totalCount: N} +``` + +**Activity log categories:** +``` +POST /users/{userId}/records/categories +{"filters": {"startDate": "2026-05-28T21:51:48.266Z"}, "includeCustomers": true} +→ [{category, displayCategory}] +``` + +--- + +## Notifications / journal + +``` +GET /journal/notifications +GET /journal/counts +GET /journal/warnings/accepted +POST /journal/acknowledge/all +POST /canopy-sites/{siteId}/notifications/acknowledge-all +``` + +**Notifications (site-level, POST — supports filtering):** +``` +POST /canopy-sites/{siteId}/notifications +{"limit": 1, "includeTotalCount": true, "filters": {"state": {"$in": ["unread", "process"]}}} +→ {records: [...]} +``` + +**Journal notifications (cross-site, filtered):** +``` +POST /journal/notifications +{ + "canopyIds": ["siteId"], + "limit": 100, "skip": 0, + "filters": {"priority": {"$in": ["critical", "high"]}, "state": {"$in": ["unread", "process"]}}, + "projection": {...} +} +→ {records: [...], total: N} +``` + +--- + +## Weather + +``` +GET /moduleGroups/getCanopyWeather + ?location[latitude]={lat}&location[longitude]={lng} +``` +Returns 3-day AccuWeather-style forecast. + +--- + +## Clusters (watering module groups) + +``` +GET /clusters/{clusterId}/data +GET /clusters/{clusterId}/modules +GET /clusters/{clusterId}/programs +PUT /clusters/{clusterId}/programs +PATCH /clusters/{clusterId}/name +PATCH /clusters/{clusterId}/flowrate +DELETE /clusters/{clusterId} +``` + +--- + +## Favorites & tags + +``` +GET /favorites/{userId} +POST /favorites +DELETE /favorites/{userId} +GET /modules/{moduleId}/tags +POST /module/tags { "module": "moduleId", "tags": [...] } +``` + +--- + +## Module notebook (event log) + +Full chronological history of manual commands, alerts, firmware updates, pool events: + +``` +GET /modules/{moduleId}/notebook?startDate={unix_epoch_seconds} +``` + +> `startDate` is a **Unix timestamp in seconds**, not ISO 8601. + +Response: `{ "records": [...] }` sorted oldest-first. + +Record structure: +```json +{ + "id": "...", + "module": "moduleId", + "category": "manual_command", + "label": "pHmètre - Seuil bas dépassé", + "businessLabel": "Le pH est trop bas", + "priority": "low", + "state": "archived", + "timestamp": "2026-06-04T21:00:00.000Z", + "params": { "action": 4, "index": 1 }, + "input": "inputId", + "notifications": [], + "comments": [] +} +``` + +Known `category` values: `manual_command`, `pool`, `platform_alert`, `firmware_update` + +The `params` field structure varies by category: for `manual_command` it mirrors the linesControl action/index that triggered the event; for `pool` it contains poolAlertType (e.g. `backwashReminder`, `poolLevelSensorCleaningReminder`). See `api-docs/control.md` for action code definitions. + +--- + +## Module users + +``` +GET /modules/{moduleId}/users +``` +Returns array of full user objects with access to this module (email, push tokens, preferences). + +--- + +## Linked inputs + +``` +GET /input/getInputsLinkedModule/{moduleId} +``` +Returns inputs linked to a module. Returns `[]` for standalone LoRa modules. + +--- + +## User avatar + +``` +GET /user/avatar +GET /user/avatar?t={unix_ms} ← cache-bust variant +``` +Returns current authenticated user's avatar as `image/png`. Never cached (`no-cache, no-store`). + +--- + +## SIM subscription + +``` +GET /users/{userId}/checkSubscriptionSimcardStatus +``` +Returns ~39-byte JSON — whether the user has an active cellular SIM subscription. + +--- + +## Field supervision (watering events over time range) + +``` +GET /canopy/{siteId}/field-supervisions/modules +→ {canopyModules: [{type, modules: [{serialNumber, name, outputs: [{id, index, name}]}]}]} + +GET /canopy/{siteId}/field-supervision/{supervisionId}/resume +→ {allPrograms: [...]} + +GET /moduleGroups/{siteId}/field-supervision/{supervisionId}/events/{startISO}/{endISO}?timezone=Europe/Paris +→ {events: [{eventId, moduleId, moduleName, identifier, name, start, end, flowRate, index}]} +``` + +Field supervision ID for Domicile: `643c5134347b27edbf38b173` + +--- + +## Satellite / NDVI imagery + +Host: **`sentinel.mysolem.com`** (separate from the main API host). + +**Check image availability:** +``` +GET /moduleGroups/{siteId}/fencing/{timestampMs}/images-check +→ {availableImages: {available: true, reason: "ok"}} +``` + +**List images:** +``` +GET /moduleGroups/{siteId}/fencing/{timestampMs}/images +→ {imagesList: {images: [{name, urlRgb, urlSavi}]}} +``` + +**Fetch satellite images directly:** +``` +GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/savi → PNG (SAVI vegetation index) +GET https://sentinel.mysolem.com/api/plot/{plotId}/image/{timestamp}/rgb → PNG (true color) +``` + +Timestamp format: `YYYYMMDDTHHmmss`, e.g. `20220417T103631`. +Domicile plot ID: `62f4efcb4a6dd563203558c3`, fencing timestamp: `1656939129360` diff --git a/api-docs/lrag.md b/api-docs/lrag.md new file mode 100644 index 0000000..e878e2b --- /dev/null +++ b/api-docs/lrag.md @@ -0,0 +1,101 @@ +# LR6AG — Agricultural Valve Controller (LR6AG-070917) + +**Module ID**: `626984f038ec558c598bb6d5` +**Serial**: `6606010709170001` +**Type**: `lr-ag` +**Role**: Controls 6 irrigation stations, has 1 rain sensor input + +--- + +## Input + +| Input ID | Name | Type | Unit | Notes | +|---|---|---|---|---| +| `626984f038ec558c598bb6d7` | *(unnamed)* | 2 (TOR/on-off) | raw | Rain sensor — value 1=rain, 0=clear | + +### Rain sensor — recent state +``` +2026-06-04T15:37 value=1 (rain detected) +2026-06-03T10:57 value=0 (clear) +2026-06-02T12:19 value=1 (rain detected) +2026-05-28T19:06 value=0 (clear) +2026-05-28T19:05 value=0 +``` + +--- + +## Outputs + +| Output ID | Name | Index | Flow rate | +|---|---|---|---| +| `626984f038ec558c598bb6d8` | Station 1 | 0 | 30 m³/h | +| `626984f038ec558c598bb6d9` | Station 2 | 1 | 14 m³/h | +| `626984f038ec558c598bb6da` | Station 3 | 2 | 20 m³/h | +| `626984f038ec558c598bb6db` | Station 4 | 3 | 1 m³/h | +| `626984f038ec558c598bb6dc` | Station 5 | 4 | 38 L/h | +| `626984f038ec558c598bb6dd` | Station 6 | 5 | 38 L/h | + +Flow rate unit: `1` = L/h, `2` = m³/h + +--- + +## Programs + +6 programs (one per station), all **reset on 2026-06-04 with no active windows**. +Programs are named Station 1–6 (type 3 = standard irrigation). + +``` +GET /programs/modules?moduleIds=626984f038ec558c598bb6d5 +``` + +--- + +## Remote state + +``` +GET /remote/module/state?moduleId=626984f038ec558c598bb6d5 +``` + +- Battery: 81% +- Last radio contact: 2026-06-04T19:57:05Z +- Relay: `60ca4276ad69dac1558f7c61` (LRMB10) + +**Live output status** (from `state.ag.outputs`): +| Station | State | Rain delay | +|---|---|---| +| 0 | off | 255 days | +| 1 | off | 255 days | +| 2 | off | 0 | +| 3 | off | 0 | +| 4 | off | 0 | +| 5 | off | 0 | + +Stations 0 and 1 have a rain delay of 255 — effectively suspended indefinitely. +`state` field: `0` = off, `1` = on/running. + +--- + +## Manual control + +``` +POST /module/sendManualModuleCommand +{ + "moduleSerialNumber": "6606010709170001", + "commandType": "station", + "outputIndex": 0, + "command": null, + "commandParams": { "hours": 0, "minutes": 15, "on": 0, "off": 0 } +} +``` + +Rain delay (suspend station): +```json +{ + "moduleSerialNumber": "6606010709170001", + "commandType": "status", + "command": "off", + "commandParams": 7, + "outputIndex": 0 +} +``` +`commandParams` = number of days to delay (255 = maximum/indefinite). diff --git a/api-docs/lripeco.md b/api-docs/lripeco.md new file mode 100644 index 0000000..3503ae9 --- /dev/null +++ b/api-docs/lripeco.md @@ -0,0 +1,79 @@ +# LR2IPECO — IP ECO Controller (LR2IPECO-0C9282) + +**Module ID**: `6516ca57b7c098e6807cb332` +**Serial**: `3802020C92820001` +**Type**: `lr-ip-eco` +**Role**: 2-station irrigation controller with flow meter input + +--- + +## Input + +| Input ID | Name | Type | Unit | Notes | +|---|---|---|---|---| +| `6516ca57b7c098e6807cb334` | *(unnamed)* | 33 (flow) | L | Flow meter — cumulative per minute | + +### Flow meter — recent readings (2026-06-04) +``` +06:12 0 L/min +06:11 2.12 L/min +06:10 0 L/min +06:09 0 L/min +06:08 1.97 L/min +``` +Pulse-based flow meter, alternates between values and 0 (on/off pattern suggests short irrigation bursts). + +--- + +## Outputs + +| Output ID | Name | Index | +|---|---|---| +| `6516ca57b7c098e6807cb336` | Station 1 | 0 | +| `6516ca57b7c098e6807cb337` | Station 2 | 1 | + +No flow rates configured (useSensor: false). + +--- + +## Programs + +3 programs: Programme A (index 0), Programme B (index 1), Programme C (index 2). +All have **no active windows** (unconfigured). + +``` +GET /programs/modules?moduleIds=6516ca57b7c098e6807cb332 +``` + +--- + +## Remote state + +``` +GET /remote/module/state?moduleId=6516ca57b7c098e6807cb332 +``` + +- Primary battery: 89% +- Secondary battery: 3200 mV (`secondaryBatteryVoltage`) — secondary power state: 0 +- Last radio contact: 2026-06-04T19:43:08Z +- Relay: `60ca4276ad69dac1558f7c61` (LRMB10) + +Live watering status (`state.watering`): +```json +{ + "origin": 0, + "runningProgram": 0, + "runningStation": 0, + "time": "00:00", + "state": 1 +} +``` +`state: 1` = currently running / active (irrigation in progress at last poll). + +--- + +## Notes + +- Remote config returns `module_not_reachable` — LoRa-only communication +- The `secondaryBatteryVoltage` field is unique to this module (likely a backup/solar battery) +- Output events endpoint returns no historical records — watering history only inferrable from flow meter data diff --git a/api-docs/lrmb10.md b/api-docs/lrmb10.md new file mode 100644 index 0000000..40d31f1 --- /dev/null +++ b/api-docs/lrmb10.md @@ -0,0 +1,32 @@ +# LRMB10 — LoRa Gateway (LRMB10-070532) + +**Module ID**: `60ca4276ad69dac1558f7c61` +**Serial**: `1000000705320001` +**Type**: `lr-mb-10` +**Role**: LoRa relay — all other modules communicate through this gateway + +--- + +## Hardware info (from `remote/module/configuration`) + +| Field | Value | +|---|---| +| Firmware | 6.5.0 | +| Hardware | 68.0.0 | +| BLE | 1.0.13 | +| WiFi MAC | `10:52:1C:01:2D:F4` | +| BT MAC | `C8:B9:61:07:05:32` | +| Ethernet MAC | `FF:FF:FF:FF:FF:FF` (not used) | +| WiFi SSID | `Freebox-2A5E5D` | +| Links | WiFi ✓, Bluetooth ✓, Ethernet ✗, Modem ✗ | +| Server | `qualif.mysolem.com` / `10.iot` | +| Uptime | 775361 s (~9 days) | +| Last reboot count | 11 | + +--- + +## Notes + +- No programs or outputs — pure relay/gateway +- All LoRa child modules (`LRMS`, `LRAG`, `LRPS`, `LRPC`) reference this via their `relay` field +- Remote config endpoint works directly: `GET /remote/module/configuration?moduleId=60ca4276ad69dac1558f7c61` diff --git a/api-docs/lrms.md b/api-docs/lrms.md new file mode 100644 index 0000000..6e609b6 --- /dev/null +++ b/api-docs/lrms.md @@ -0,0 +1,73 @@ +# LRMS — Sensor Station (LRMS-0721DC) + +**Module ID**: `60ca42adad69dac1558f7c64` +**Serial**: `7400040721DC0001` +**Type**: `lr-ms` +**Role**: Pure sensor station — no outputs, no programs + +--- + +## Inputs + +| Input ID | Name | Type | Unit | Interval | Notes | +|---|---|---|---|---|---| +| `60ca42adad69dac1558f7c66` | Plumbago | 12 (soil moisture) | % | 60 min | Named sensor | +| `60ca42adad69dac1558f7c67` | Pelouse | 12 (soil moisture) | % | 60 min | Named sensor | +| `60ca42adad69dac1558f7c69` | *(unnamed)* | 5 (temperature) | °C | 15 min | Air/soil temp | + +### Latest readings (2026-06-04) + +**Plumbago** (moisture) +``` +19:42 71.19% +18:42 74.06% +17:42 71.63% ← irrigation spike visible +16:42 44.64% +15:42 41.83% +``` + +**Pelouse** (moisture) +``` +19:42 78.25% +18:42 79.63% +17:42 70.81% ← irrigation spike visible +16:42 58.50% +14:42 56.19% +``` + +**Temperature (unnamed)** +``` +19:42 16.10°C +19:27 16.44°C +18:42 16.66°C +18:12 16.54°C +17:42 16.31°C +``` + +--- + +## Remote state + +``` +GET /remote/module/state?moduleId=60ca42adad69dac1558f7c64 +``` + +- Battery: 78% (batteryVoltage field) +- Last radio contact: 2026-06-04T19:51:50Z +- Relay: `60ca4276ad69dac1558f7c61` (LRMB10) +- `relayIsLongPolling: true` + +--- + +## Sensor data endpoint + +``` +GET /remote/input/getComputedSensorData + ?inputId={inputId} + &startDate={iso8601} + &endDate={iso8601} + &ensurePrevTickForContinuity=true +``` + +The moisture sensor uses a computed expression to convert raw ADC values to %. +Moisture spikes (>10% jump) correspond to irrigation events — useful to infer watering history even without direct output events. diff --git a/api-docs/lrniv.md b/api-docs/lrniv.md new file mode 100644 index 0000000..655a916 --- /dev/null +++ b/api-docs/lrniv.md @@ -0,0 +1,82 @@ +# LRLEVEL — Pool Fill Valve Controller (LRLEVEL-0D2ED9) + +**Module ID**: `6a1c4b153d6c33ab843edc13` +**Type**: `lr-niv` (niveau = level) +**Role**: Controls pool water fill valve; measures volume added and fill state + +--- + +## Inputs + +| Input ID | Type | Unit | Description | +|---|---|---|---| +| `6a1c4b153d6c33ab843edc15` | 33 (flow) | L | Volume of water added per fill cycle | +| `6a1c4b153d6c33ab843edc16` | 149 (level state) | raw | Fill valve / level state (0/1) | + +### Input edc15 — Water volume (fill meter) + +Expression: `javascript:if(x<=0){0.0}else if(x>=80000){68.76}else{-5.01×10⁻¹⁰×...}` (polynomial curve for pulse-to-litre conversion) + +Recent readings — all **0 L** for the past 5 ticks (no fill events since 2026-06-03): +``` +2026-06-04T19:12 0 L +2026-06-04T13:11 0 L +2026-06-04T07:10 0 L +2026-06-04T01:09 0 L +2026-06-03T19:08 0 L +``` +Pool level currently stable — no refill needed. + +### Input edc16 — Level state + +Type 149 (new), raw 0/1. Consistently reads **1** in all recent ticks (every 6h): +``` +2026-06-04T15:12 1 +2026-06-04T09:11 1 +2026-06-04T03:10 1 +2026-06-03T21:09 1 +2026-06-03T15:08 1 +``` +`1` = fill valve closed / level OK. + +--- + +## Output + +| Output ID | Name | Index | +|---|---|---| +| `6a1c4b153d6c33ab843edc17` | Station 1 | 0 | + +Controls the physical fill valve. Program index 0 ("Station 1") exists but has no active windows — fill is triggered automatically by the level sensor, not by schedule. + +--- + +## Remote state + +- Battery: **94%** +- Last radio contact: 2026-06-04T20:12:35Z +- Relay: `60ca4276ad69dac1558f7c61` (LRMB10) + +--- + +## Fill event detection + +To detect when the pool was last filled and how much water was added, query the flow input over a wider date range and filter for ticks where `value > 0`: + +```python +data = session.get(f"{BASE}/remote/input/getComputedSensorData", params={ + "inputId": "6a1c4b153d6c33ab843edc15", + "startDate": "2026-01-01T00:00:00.000Z", + "endDate": "2026-06-04T23:59:59.000Z", + "ensurePrevTickForContinuity": "true", +}).json() + +fill_events = [t for t in data["computedSensorData"] if t["value"] > 0] +``` + +--- + +## Notes + +- Type 149 is a new input type not seen in other modules (likely lr-niv specific level state) +- The system works as: `pool-level-sensor` (float) → signals low → `LRLEVEL` opens fill valve (output 0) → `edc15` measures L added → valve closes when `pool-level-sensor` returns 1 diff --git a/api-docs/lrpc.md b/api-docs/lrpc.md new file mode 100644 index 0000000..cfab972 --- /dev/null +++ b/api-docs/lrpc.md @@ -0,0 +1,121 @@ +# LRPC — Pool Controller (LRPC-F1DA27) + +**Module ID**: `6481bcea8782b78554c02bed` +**Serial**: `150302F1DA270001` +**Type**: `lr-pc` + +--- + +## Pool configuration + +| Field | Value | +|---|---| +| Pool ID | `5de53c1c7ed4c45c2cfe0688` | +| Volume | 25 m³ | +| Shape | rectangular 6 × 3 m, depth 1.4 m | +| Filter | glass filter | +| Treatment | stabilised chlorine tablets | +| Chlorine regulation | manual | +| pH regulation | automatic | +| Installation | outdoor, no heating, no cover | + +Endpoint: +``` +GET /maintenance-logs-data?moduleId=6481bcea8782b78554c02bed +``` +Returns 25+ pool alerts, maintenance history, and `messages[0].pool` with full config. + +--- + +## Sensors / Inputs + +| Input ID | Metric | Unit | Type code | +|---|---|---|---| +| `6481bcea8782b78554c02bef` | Water temperature | °C | 133 | + +> Sensor telemetry endpoint: `GET /remote/input/getComputedSensorData?inputId=...` +> Live readings (temp, pH, ORP, pressure) are in `maintenance-logs-data` under `pool.status`. + +--- + +## Outputs + +| Output ID | Name | Index | +|---|---|---| +| `6481bcea8782b78554c02bf1` | Station 1 (filtration pump) | 0 | +| `6481bcea8782b78554c02bf2` | Station 2 | 1 | +| `6481bcea8782b78554c02bf3` | Station 3 | 2 | + +--- + +## Filtration programs + +### Program 0 — Filtration (type 4, index 0) + +ID: `6481bcea8782b78554c02bf4` +Runs every day (weekDays: 127). + +| Window | Time range | Duration | +|---|---|---| +| Morning | 05:00–06:00 | 1h | +| Midday | 12:00–14:00 | 2h | +| Evening | 22:00–23:00 | 1h | +| **Total/day** | | **4h** | + +Special features: +- **Micro-filtration** (frost mode): triggers every 480 min for 10 min when temp < 5°C +- **Temperature-adaptive schedules**: 2 schedules active (96 days + 31 days/year), each with 12 thresholds that adjust run windows based on pool temperature + +### Program 1 — Auxiliary/cyclic (type 2, index 1) + +ID: `6481bcea8782b78554c02bf5` +Active window: 21:30–22:30 (1h slot), cycle duration 5 min. +Likely electrolyser or secondary pump. + +### Program 2 — Unused (index 2) + +ID: `6481bcea8782b78554c02bf6` +No active windows configured. + +--- + +## Reading filtration schedule + +``` +GET /programs/modules?moduleIds=6481bcea8782b78554c02bed +``` + +Window times are in **minutes from midnight**. Duration (`on`) is in **seconds**. + +```python +def mins_to_time(m): return f"{m//60:02d}:{m%60:02d}" +def secs_to_human(s): h,m = divmod(s,3600); return f"{h}h{m//60:02d}m" + +for prog in data['programs']: + for w in prog['windows']: + if w['runningDays'] > 0: + print(f"{mins_to_time(w['start'])}–{mins_to_time(w['end'])} {secs_to_human(w['on'])}") +``` + +--- + +## Alerts & maintenance history + +``` +GET /maintenance-logs-data?moduleId=6481bcea8782b78554c02bed +``` + +Returns `{ code, messages: [...] }`. Each message has: +- `poolAlertType`: `backwashReminder`, `backwashDone`, `poolLevelSensorCleaningReminder`, … +- `priority`: `low` / `medium` / `high` +- `state`: `read` / `unread` +- `timestamp`: ISO 8601 +- `pool`: full pool config object (only on messages that have pool context) + +--- + +## Notes + +- `remote/module/configuration` and `remote/module/informations` return `module_not_reachable` — the LRPC communicates via LoRa relay, not direct HTTP +- `output/events` returns empty — past filtration run history is not stored server-side per event; the program schedule is the source of truth +- The LRPC temperature input (type 133, unit 2) is distinct from the LRPS pool sensor temperature; both track pool water temp but from different probes diff --git a/api-docs/lrpr.md b/api-docs/lrpr.md new file mode 100644 index 0000000..e4aee36 --- /dev/null +++ b/api-docs/lrpr.md @@ -0,0 +1,66 @@ +# LRPR — Filter Pressure Sensor (LRPR-0D6800) + +**Module ID**: `6a17218be0b88ea3e925524f` +**Serial**: `1D00010D68000001` +**MAC**: `C8:B9:61:0D:68:00` +**UUID**: `1D0058B9610D680000000001000D6800` +**Firmware**: 6.3.0 | **Hardware**: A | **Manufacturer**: `1D` +**Type**: `lr-pr` +**Role**: Measures filter pressure — indicates filter cleanliness / pump running state + +--- + +## Input + +| Input ID | Type | Unit | Interval | +|---|---|---|---| +| `6a17218be0b88ea3e9255251` | 8 (pressure) | bar (unit code 6) | 1 min | + +Raw-to-bar expression: `(1.5 × raw − 500) / 1000` + +### Latest readings (2026-06-04) + +``` +16:01 0.091 bar ← pump stopped / off +16:00 0.715 bar +15:59 0.741 bar +15:58 0.730 bar +18:41 0.081 bar ← pump off +``` + +Normal operating pressure ~0.70–0.75 bar. Drop to ~0.08 bar signals pump has stopped. +This sensor can be used to detect filtration start/stop and detect dirty filter (rising pressure). + +--- + +## Remote state + +- Battery: **94%** +- Last radio contact: 2026-06-04T20:05:14Z +- Relay: `60ca4276ad69dac1558f7c61` (LRMB10) +- No programs, no outputs — pure sensor + +--- + +## Pressure Alert Configuration + +From `poolPressureAlertsVariables` in the input object (retrieved via mobile API): + +| Parameter | Value | Meaning | +|---|---|---| +| `staticPressure` | 0.919 bar | Baseline pressure at pump start | +| `nominalPressure` | 1.7275 bar | Expected operating pressure | +| `maximalPressure` | 1.0615 bar | Upper alert threshold | +| `atmosphericPressure` | 1.015 bar | Calibration reference | +| `filterCloggedMargin` | 0.5 bar | Rise above static = filter clogged alert | +| `filterPreCloggedMargin` | 0.3 bar | Rise above static = pre-clog warning | +| `activeFilterCloggedAlert` | true | Filter clog alert enabled | +| `activeWaterLevelLowAlert` | true | Low water level alert enabled | +| `activeSuctionValveClosedAlert` | true | Suction valve closed alert enabled | +| `waterLevelLowCount` | 6 | Ticks below threshold before alert | + +## Notes + +- `secondaryBatteryVoltage: 80` — secondary power source present +- Unit code 6 = bar; type 8 = pressure sensor +- Backwash reminder alerts fire every ~15 days (shared with pool notebook) diff --git a/api-docs/lrps.md b/api-docs/lrps.md new file mode 100644 index 0000000..6e39b04 --- /dev/null +++ b/api-docs/lrps.md @@ -0,0 +1,69 @@ +# LRPS — Pool Analyser / Master Valve (LRPS-0565C0) + +**Module ID**: `6480da07c2c0896da120ecca` +**Serial**: `9400030565C00001` +**Type**: `lr-mas` +**Role**: Pool water quality analyser (temperature, pH, ORP/redox) + +--- + +## Inputs + +| Input ID | Metric | Type | Unit | Interval | +|---|---|---|---|---| +| `6480da07c2c0896da120eccc` | Water temperature | 5 | °C | ~30 min | +| `6480da07c2c0896da120eccd` | pH | 6 | — | varies | +| `6480da07c2c0896da120ecce` | ORP / Redox | 7 | mV | varies | + +### Latest readings (2026-06-04) + +**Temperature** +``` +19:03 23.43°C +18:03 23.56°C +17:03 23.75°C +16:33 23.87°C +15:33 24.12°C +``` +Peak today: 24.62°C at 12:33, cooling through afternoon. + +**pH** +``` +2026-06-04T01:03 6.35 +2026-06-03T12:33 6.30 +2026-06-03T03:03 6.35 +2026-06-02T18:03 6.30 +2026-06-01T20:03 6.25 +``` +Slowly rising from 5.98 (2026-05-30). Ideal range: 7.2–7.6. Currently below target. + +**ORP / Redox** +``` +2026-06-04T16:33 646 mV +2026-06-04T15:33 666 mV +2026-06-04T15:03 661 mV +2026-06-04T14:33 651 mV +2026-06-04T14:03 646 mV +``` +Healthy range: 650–750 mV. Currently borderline. + +--- + +## Remote state + +``` +GET /remote/module/state?moduleId=6480da07c2c0896da120ecca +``` + +- Battery: 60% (low — alerts were sent Feb 2026) +- Last radio contact: 2026-06-04T20:06:55Z +- Relay: `60ca4276ad69dac1558f7c61` (LRMB10) + +--- + +## Alerts (from LRPC maintenance-logs-data) + +| Date | Alert | +|---|---| +| 2026-03-18 | Analyzer LRPS-0565C0 needs calibration | +| 2026-02-03 | Battery too low — measurements may be unreliable | diff --git a/api-docs/mobile-api.md b/api-docs/mobile-api.md new file mode 100644 index 0000000..22cc02b --- /dev/null +++ b/api-docs/mobile-api.md @@ -0,0 +1,343 @@ +# Mobile App API (iOS — MySOLEM & MyIndygo apps) + +**Base URL**: `https://qualif.mysolem.com` +**Prefix**: `/api/` (distinct from the web platform's unprefixed routes) +**Auth**: OAuth2 Bearer token (not session cookie) +**Source**: mitmproxy capture of both iOS apps (`inputs/apps_captured_all.jsonl`) + +The mobile apps use a cleaner, more structured REST API than the web platform. Key differences: +- Bearer token instead of cookies +- `/api/` prefix on all endpoints +- Gateway-routed module access: `GET /api/module/{gatewaySerial}/{action}/{childSerial6}` +- High-level composite endpoints (`getPoolStatus`, `getUserWithHisModules`) + +--- + +## Authentication — OAuth2 + +``` +POST /oauth2/token +Content-Type: application/json + +{"scope": "*", "grant_type": "client_credentials"} +``` + +Response: +```json +{ + "access_token": "6wtff8dVlhjn7y...", + "expires_in": 5184000, + "token_type": "Bearer" +} +``` + +`expires_in` = 5,184,000 seconds = **60 days**. + +The `client_id` and `client_secret` are embedded in the app binary. The server accepts `client_credentials` without explicit client authentication headers visible in the capture — likely validated by the app's TLS client certificate or embedded credentials. + +All subsequent requests: +``` +Authorization: Bearer {access_token} +``` + +```python +import requests + +BASE = "https://qualif.mysolem.com" +r = requests.post(f"{BASE}/oauth2/token", json={"scope": "*", "grant_type": "client_credentials"}) +token = r.json()["access_token"] +headers = {"Authorization": f"Bearer {token}"} + +# Then use headers= on every request +resp = requests.get(f"{BASE}/api/getUser", headers=headers) +``` + +--- + +## User + +``` +GET /api/getUser +``` +Returns current authenticated user with `agency`, `distributor`, `manufacturer` fields not present in the web API. + +``` +GET /api/getUserWithHisModules +``` +Returns user object with full `modules` array embedded. + +``` +GET /api/getUserPools +``` +Returns array of pool configuration records. Each pool has: +```json +{ + "id": "5de53c1c7ed4c45c2cfe0688", + "owner": "5de53bca7ed4c45c2cfe067f", + "adminUsers": ["5de53bca7ed4c45c2cfe067f"], + "address": "30 Rue de Florette, 30250 Villevieille, France", + "latitude": 43.79417821332963, + "longitude": 4.095361891348177, + "volume": 25, + "installationType": "outdoor", + "filteringType": "filterGlass", + "waterTreatmentType": "chlorineStabilizedTablet", + "phRegulationMode": "automatic", + "chlorineRegulationMode": "manual", + "heatingSystemType": "none", + "coverType": "none", + "cyanuricAcidRate": 0, + "model": "Autre" +} +``` + +> This endpoint reveals the pool ID `5de53c1c7ed4c45c2cfe0688` for the Domicile pool — previously only accessible via the main `anelissen@solem.fr` account on the web. Via the mobile API + OAuth2 it is readable. + +``` +GET /api/getUserWateringModuleGroups → {"wateringModuleGroups": []} +GET /api/getUserAgriculturalModuleGroups → {"agriculturalModuleGroups": []} +GET /api/getCustomers → {"err": {"code": "user_is_not_professional"}} (pro-only) +GET /api/users/{userId}/canopy-sites +GET /api/users/{userId}/access-requests +GET /api/users/{userId}/module-access-requests +``` + +--- + +## Pool + +``` +GET /api/getPoolStatus?attributesToPopulate[]=modules&poolIdentifier={poolId} +``` +Returns pool status including all associated modules with full config. Pool ID for Domicile: `5de53c1c7ed4c45c2cfe0688`. + +``` +GET /api/getUserPools (see above) +``` + +--- + +## Modules + +``` +GET /api/getModuleWithHisUsers?module={moduleId} +``` +Returns module object with full `users` array. + +``` +GET /api/getModuleInventory + ?module={gatewaySerial} + &startDate={iso8601} + &endDate={iso8601} + &data=1 +``` +Returns `{children: [...]}` — all child modules registered under this gateway with full config, battery, connectivity state. Most efficient way to enumerate a site's devices. + +``` +GET /api/getModuleBackups?module={moduleId} +``` +Returns array of saved program backups (automatic + manual snapshots). + +``` +PUT /api/updateModule +Content-Type: application/json + +{"module": {"id": "moduleId", "batteryVoltage": 77, "powerState": 0, "status": {...}, ...}} +``` +Response: full module object including `inputs` and `outputs` arrays. +Used by the app to sync device status back to the server after a LoRa radio round-trip. The `status` field contains the device's reported state (e.g. `ag.outputs`, `watering`). + +--- + +## Gateway-routed module access + +All real-time commands go through the gateway module (LRMB10). The URL encodes: +- `{gatewaySerial}` = full gateway serial, e.g. `1000000705320001` +- `{childSerial6}` = **last 6 hex chars** of the child module's serial + +**Domicile module routing table:** + +| Child module | childSerial6 | Full serial | +|---|---|---| +| LRPC-F1DA27 (pool controller) | `F1DA27` | `150302F1DA270001` | +| LR6AG-070917 (valve) | `070917` | `6606010709170001` | +| LRMS-0721DC (sensor station) | `0721DC` | `7400040721DC0001` | +| LR2IPECO-0C9282 (IP ECO) | `0C9282` | `3802020C92820001` | +| LRLEVEL-0D2ED9 (fill valve) | `0D2ED9` | `3A01020D2ED90001` | +| LRPR-0D6800 (pressure) | `0D6800` | (lr-pr type) | + +### Read endpoints + +``` +GET /api/module/{gatewaySerial}/information ← gateway itself +GET /api/module/{gatewaySerial}/information/{child6} ← child module real-time info +GET /api/module/{gatewaySerial}/status/{child6} ← current output/input states +GET /api/module/{gatewaySerial}/configuration/{child6} ← device configuration +GET /api/module/{gatewaySerial}/programs/{child6} ← programs stored on device +GET /api/module/{gatewaySerial}/inventory ← all children (same as getModuleInventory) +``` + +Gateway information response (`GET /api/module/1000000705320001/information`): +```json +{ + "mtype": "10", + "msn": "1000000705320001", + "name": "LRMB10-070532 (Perso)", + "vsoft": "6.5.0", + "ble": "1.0.13", + "ssid": "Freebox-2A5E5D", + "bluetoothLink": true, + "wifiLink": true, + "ethernetLink": false, + "modemLink": false, + "up": 782800, + "cdate": "2026-06-05T00:11:38+02:00", + "dmn": "qualif.mysolem.com" +} +``` + +### Write endpoints + +``` +POST /api/module/{gatewaySerial}/status/{child6} +Content-Type: application/json + +{"ag": [{"index": 0, "rainDelay": 0}, {"index": 1, "rainDelay": 0}, ...]} +``` +Sets rain delay for each output. Response: full device status including `dialogTimeStamp`, `ackTimestamp`, `temperature`, `battery`, `ag.sensor`, `ag.outputs`. + +``` +POST /api/module/{gatewaySerial}/programs/{child6} +Content-Type: application/json + +{programs: [...], programmationType: 1} +``` +Sends programs directly to device over LoRa. + +``` +POST /api/module/{gatewaySerial}/manual/{child6} +Content-Type: application/json + +{...command body...} +``` +Sends a manual command to the device over LoRa. + +--- + +## Sensor data & chart + +``` +GET /api/getInputs?id={moduleId} +``` +Returns all inputs for a module with full `expression`, `expressionInverse`, alert config, `pushAlert`, `mailAlert`. + +``` +GET /api/getComputedChartData + ?inputId={inputId} + &startDate={iso8601} + &endDate={iso8601} + &numberOfBar={N} + &locale=fr +``` +Returns input data bucketed into N bars over the date range. Response includes `expression`, `expressionInverse`, and `computedSensorData` array. + +``` +GET /api/getComputedOutputActivationHistory + ?moduleId={moduleId} + &outputIndex={0|1|2...} + &startDate={iso8601} + &endDate={iso8601} + &numberOfBar={N} +``` +Returns per-period activation state: `[{currentPeriodStartDate, currentPeriodEndDate, value}]`. +With `numberOfBar=1440` over 24h = 1 bar per minute. + +--- + +## Programs + +``` +GET /api/getPrograms?id={moduleId} +``` +Full program objects including `startTimesCycles`, `stationsDuration` arrays not present in the web API. + +``` +PUT /api/updatePrograms +Content-Type: application/json + +{ + "programmationType": 1, + "programs": [{ + "id": "...", + "name": "...", + "waterBudget": 100, + "windows": [...], + "programCharacteristics": {...} + }] +} +``` +Response: `{updatedPrograms: [...]}`. + +``` +POST /api/library/program-templates +{"limit": 10, "moduleId": "moduleId", "offset": 0} +→ {"programTemplates": []} +``` + +--- + +## Documents & metadata + +``` +GET /api/getDocument?documentType=installation-guide&product-type={moduleType} +→ {"url": "https://qualif.mysolem.com/files/notices/{type}/solem/fr-installation-guide.pdf"} +``` + +``` +GET /api/getAdvertisingProfilePresets +→ [{name, ble: {windows: [...]}, loRa: {interval}}] +``` +BLE advertising profiles: `default`, `ecoMode`, etc. + +``` +GET /api/getAllBanishedSerialNumbers +→ ["120C0200750001", ...] +``` +Blacklisted/revoked device serial numbers. + +--- + +## Push notifications + +``` +POST /api/addApnsTokenToUser +{"isSandbox": false, "topic": "com.mysolem.qualif", "user": "{userId}", "token": "{apnsToken}"} +→ {"code": "ok"} +``` + +--- + +## Reporting (app telemetry) + +These are called by the app after sending data to devices — informational only. + +``` +POST /api/reportModuleDatasSent {"module": "moduleId"} +POST /api/reportProgramsDatasSent {"programs": ["programId", ...], "module": "moduleId"} +POST /api/reportManualCommandSent {"route": "http", "command": {...}, "id": "moduleId"} +``` + +--- + +## Key differences vs web API + +| | Web (cookie auth) | Mobile (Bearer token) | +|---|---|---| +| Auth | Session cookie | OAuth2 Bearer | +| Module routing | Direct by ID | Via gateway serial | +| Pool access | Admin-only / main account | `GET /api/getUserPools` | +| Sensor data | `getComputedSensorData` (ticks) | `getComputedChartData` (bars) | +| Output history | `output/events` | `getComputedOutputActivationHistory` | +| Programs | `programs/modules` | `getPrograms` (richer fields) | +| Module inventory | `modules/search` | `getModuleInventory` via gateway | +| Backups | — | `getModuleBackups` | diff --git a/api-docs/myindygo.md b/api-docs/myindygo.md new file mode 100644 index 0000000..3c382e7 --- /dev/null +++ b/api-docs/myindygo.md @@ -0,0 +1,367 @@ +# MyIndygo — Platform Overview & API Reference + +**Base URL**: `https://qualif.myindygo.com` +**Purpose**: Pool-focused platform — filtration scheduling, water chemistry, pool level monitoring +**Relation to MySOLEM**: Same backend database and Sails.js server. Both platforms share all module data, sensor readings, and programs. MyIndygo adds a pool admin/management layer on top. + +--- + +## Authentication + +Identical to MySOLEM — same cookie name, same credentials work on both platforms. + +```bash +curl -c cookies.txt https://qualif.myindygo.com/login +curl -c cookies.txt -b cookies.txt \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "email=YOUR_EMAIL&password=YOUR_PASSWORD" \ + "https://qualif.myindygo.com/login" +``` + +Cookie: `qualification-mysolem-solem-irrigation-platform.sid` (same name as MySOLEM) + +--- + +## User IDs — MyIndygo vs MySOLEM + +The test API account (`arnaud.nelissen.api@icloud.com`) has **different ObjectIds** on each platform: + +| Platform | User ID | +|---|---| +| MySOLEM | `5de53bca7ed4c45c2cfe067f` | +| MyIndygo | `6a21c3db5ebe0fa5f7885460` | + +The MyIndygo user ID appears in the page HTML: +```html + +``` + +The pool/site ID `5de53c1c7ed4c45c2cfe0688` is the main account's pool record on MyIndygo — it's only accessible when logged in as the main account (`anelissen@solem.fr`), not the test API account. + +--- + +## Feature flags (MyIndygo-specific) + +`GET /features` returns pool-focused flags including: + +| Flag | Value | Meaning | +|---|---|---| +| `ocedisWaterChemistryCorrection` | `true` | Water chemistry correction (OCEDIS dosing) | +| `authenticationMyIndygoUIV2` | `true` | MyIndygo-specific login UI | +| `enableBSTSubscriptions` | `true` | BST subscription management | +| `enablePWAIndygoInstallation` | `true` | Pool app PWA install | +| `clusterTurfOpened` | `true` | Turf cluster watering | +| `enablePolytropicAPI` | `true` | Polytropic pump integration | +| `enableGraphicalProgrammation` | `true` | Visual program editor | +| `enableNdviSatellite` | `true` | NDVI satellite data | +| `enableEvapotranspiration` | `true` | ET-based watering | +| `enableHPA` | `true` | HPA (high-pressure assist?) | + +--- + +## Pool Admin System (B2B) + +MyIndygo has a pool dealer admin layer for managing multiple customer pools. These endpoints are **admin-only** — not accessible with a standard user account. + +``` +GET /pools/admin list all managed pools +POST /pools/admin/search search pools (DataTables format) +POST /pools/admin/sendMessage send a message to a pool customer +GET /pools/admin/getMessages get admin messages +DELETE /pools/admin/dissociateAndRemove/{id} remove pool from admin +``` + +Pool data is fetched via: +``` +GET /user/pools current user's pools (returns {"pools": []} for test account) +GET /user/pools?userId= another user's pools (IDOR — no ownership check) +``` + +--- + +## Pool-Specific Endpoints + +### Last sensor tick (no date range needed) +``` +GET /input/getLastTickFromInput?inputId={inputId} +``` +Returns the single most-recent raw tick for an input. Much lighter than `getComputedSensorData`. + +Response: +```json +{ + "tick": { + "input": "6480da07c2c0896da120eccd", + "value": 635, + "timestamp": "2026-06-04T01:03:00.000Z", + "createdAt": "2026-06-04T01:03:11.000Z", + "id": "..." + } +} +``` + +Note: `value` is **raw** (not expression-computed). For LRPS pH: `635` = 6.35 pH (divide by 100). For LRPS temp: `2331` = 23.31°C. + +### Live pool sensor snapshot + +```python +import requests + +BASE = "https://qualif.myindygo.com" +s = requests.Session() +s.get(f"{BASE}/login") +s.post(f"{BASE}/login", data={"email": "...", "password": "..."}) + +POOL_INPUTS = { + "temp": "6480da07c2c0896da120eccc", + "pH": "6480da07c2c0896da120eccd", + "ORP": "6480da07c2c0896da120ecce", + "pressure": "6a17218be0b88ea3e9255251", + "level": "6a174dd1e0b88ea3e925531e", +} + +readings = {} +for name, input_id in POOL_INPUTS.items(): + r = s.get(f"{BASE}/input/getLastTickFromInput", params={"inputId": input_id}).json() + readings[name] = r.get("tick", {}) + +# Current values (raw): +# temp → divide by 100 → °C +# pH → divide by 100 → pH units +# ORP → raw mV +# pressure → raw ADC (apply expression for bar) +# level → 0=low/fill needed, 1=OK +``` + +### Output events history +``` +GET /output/events?moduleId={moduleId} +GET /output/events?outputId={outputId} +GET /output/events?moduleId={id}&startDate={iso8601}&endDate={iso8601} +``` +Returns `[]` if no events or empty. Tracks when outputs have been triggered. + +### Output watering forecast +``` +GET /output/forecast?moduleId={moduleId} +GET /output/forecast?outputId={outputId} +``` +Returns upcoming scheduled watering windows. + +### Real-time module informations (via LoRa) +``` +GET /remote/module/informations + ?moduleId={relayModuleId} + &childrenSerialNumber={childSerialNumber} +``` +Or via socket.io (as seen in JS): +```js +io.socket.request({ + method: "get", + url: "/remote/module/informations", + data: { + moduleId: relayId, // LRMB10 gateway ID + moduleUuid: uuid, + moduleSerialNumber: serialNumber + } +}, callback) +``` +Returns real-time device state polled over LoRa. Fails with `module_not_reachable` if device is offline. + +### Real-time combined config + programs +``` +GET /remote/module/configuration/and/programs?moduleId={moduleId} +``` +Returns configuration + programs in a single LoRa roundtrip. Requires device online. + +### Pool controller control (linesControl) +JSON body (verified from real network capture): +``` +POST /remote/module/control +Content-Type: application/json +``` +```json +{ + "moduleSerialNumber": "150302F1DA270001", + "linesControl": [ + { + "index": 1, + "action": 2, + "time": "30:00" + } + ] +} +``` + +Known `action` codes for LRPC pool controller: + +| action | meaning | extra params | +|---|---|---| +| `0` | Stop pump output (index 0 only) | — | +| `1` | **Stop / cancel** any running manual command | — | +| `2` | Timed impulse | `time`: `"MM:SS"` string (e.g. `"30:00"` = 30 minutes) | +| `3` | Boost filtration | `time`: `"MM:SS"` | +| `4` | Start indefinitely | — | +| `5` | Pause N days (blocks manual + scheduled) | `manualDuration`: N (integer days) | +| `11` | Backwash cycle | `time`: encoded duration string, `speed`: pump speed level | + +Response pool state includes `"pause": N` (days remaining) when an output is paused. +Response always reflects **pre-command state** — the command takes effect on the next LoRa downlink. + +For backwash, duration is encoded using `encodeBackwashDuration(durationMinutes, minMinutes, maxMinutes)`. + +> **Corrected from JS analysis**: duration is `time: "MM:SS"` (string), NOT `manualDuration` in seconds. Verified from real Safari capture. + +### Module advertising profile +``` +GET /remote/module/advertisingProfile?moduleId={moduleId} +GET /remote/modules/advertisingProfile (bulk) +``` +BLE advertising profile data. Likely used for Bluetooth pairing. + +### Team pool access +``` +GET /teams/pools/{poolId}/membersAccessGranted +``` +Returns `{ teamMembersAlreadyGrantedPool: [...] }` — team members with access to a specific pool. + +--- + +## LRPC Pool Controller — Program Structure (MyIndygo extended) + +MyIndygo exposes more program fields than MySOLEM for `lr-pc` modules: + +``` +GET /programs/modules?moduleIds={moduleId} +``` + +```json +{ + "programs": [ + { + "id": "6481bcea8782b78554c02bf4", + "name": "", + "module": "6481bcea8782b78554c02bed", + "index": 0, + "weekDays": 127, + "numberOfDays": 2, + "synchroDay": 0, + "waterBudget": 100, + "startTimes": [-1, -1, -1, -1, -1, -1, -1, -1], + "programCharacteristics": { + "programType": 4, + "mode": 2, + "onSpeed": 1, + "speedSequence": 4368, + "microFilteringTemperature": 5, + "microFilteringDuration": 10, + "microFilteringPeriod": 480, + "microFilteringSpeed": 1, + "auxiliaryCyclicDuration": 0, + "frostFreeSpeed": 1, + "defaultProgramSpeed": 1, + "adaptOffset": 0, + "isShutterDependant": false, + "isRandom": false, + "isUsingButton": false + }, + "windows": [ + { + "start": 300, "end": 360, + "on": 3600, "off": 0, + "runningDays": 127 + } + ] + } + ] +} +``` + +**`programType` codes** (pool controller): + +| code | meaning | +|---|---| +| `4` | Main filtration (pump runs on schedule) | +| `2` | Auxiliary / cyclic (treatment dosing, UV, etc.) | +| `0` | Disabled / empty slot | + +**`mode`**: `2` = active, `0` = disabled +**`speedSequence`**: pump speed bitmask for variable-speed pumps (`4368` = 0x1110) +**`windows[].runningDays`**: per-window day bitmask (127 = every day, unlike mysolem where weekDays is per-program) +**`startTimes`**: array of 8 fixed start times in minutes from midnight (-1 = unused) + +### Domicile LRPC filtration schedule + +| Program | Type | Windows | Total | +|---|---|---|---| +| 0 (Filtration) | 4 | 05:00–06:00 (1h), 12:00–14:00 (2h), 22:00–23:00 (1h) | 4h/day | +| 1 (Auxiliary) | 2 | 21:30–22:30 (1h) | 1h/day | +| 2 | — | empty | — | + +--- + +## LRPC Outputs (Pool Controller) + +| Output ID | Name | Index | +|---|---|---| +| `6481bcea8782b78554c02bf1` | Station 1 | 0 | +| `6481bcea8782b78554c02bf2` | Station 2 | 1 | +| `6481bcea8782b78554c02bf3` | Station 3 | 2 | + +--- + +## New Module Types (found via modules/search on MyIndygo) + +| Type | Name | Description | +|---|---|---| +| `lr-pc-vs` | LRPCVS | Pool controller with variable speed pump | +| `lr-mv` | LRMV/LRPM1 | Motor valve (multi-output) | +| `lr-mb-30` | LRMB30 | LoRa gateway v30 | +| `lr-mb-100` | LRMB100 | LoRa gateway v100 | +| `lr-ip-fl` | LR1IPFL | IP flow metering | +| `lr-ip-fl-v2` | LR6IP | IP flow v2 | +| `lr-ip-wan-v2` | LRIP6 WAN | IP WAN v2 | +| `lr-is` | LR6IS/LR12IS | LoRa input sensor (multi-input) | +| `lr-is-city` | LR6IS | City-variant input sensor | +| `lr-is-city-wan` | LR12IS | City WAN sensor | +| `lr-ms-eco` | LRMS_ECO | Eco sensor station | +| `lr-ms-rs485` | LRMSRS485 | RS485 sensor station | +| `lr-ms-v2` | LRMS v2 | Sensor station v2 | +| `lr-ol` | LROL4 | LoRa OL (lighting?) | +| `lr-bst-100` | LRBST100 | BST 100 (booster?) | +| `lr-bst-react-4g` | LR-BST R4G | BST 4G reactive (booster 4G) | +| `joro` / `joro-v2` | WB-xxxx | Joro water balance meter | +| `smart-is` | SmartIS | Smart input sensor | +| `smart-is-lcd` | VILLA-/SMT-HOME | Smart IS with LCD display | +| `bl-ip-v2` | BL6IP | Bluetooth IP v2 | +| `ipx` | GEN_xxxx | IPX controller (generic) | + +--- + +## Shared Endpoints (same on both MySOLEM and MyIndygo) + +All endpoints from `api-docs/endpoints.md` work identically on `https://qualif.myindygo.com`. The backend database is shared — module IDs, input IDs, programs, and sensor data are identical on both platforms. + +Key shared endpoints that work well for pool monitoring: +- `GET /remote/input/getComputedSensorData?inputId=...` — time-series sensor data +- `GET /remote/module/getComputedSensorsData?moduleId=...` — all inputs for a module +- `POST /module/sendManualModuleCommand` — send commands to devices +- `GET /modules/{moduleId}/notebook?startDate={unix_epoch}` — event log +- `GET /programs/modules?moduleIds=...` — read/write programs + +--- + +## Pool Monitoring Quick Reference + +Current pool sensor IDs and live values (2026-06-04): + +| Sensor | Input ID | Last Raw Value | Computed | +|---|---|---|---| +| Water temp (LRPS) | `6480da07c2c0896da120eccc` | 2331 | 23.31 °C | +| pH (LRPS) | `6480da07c2c0896da120eccd` | 635 | 6.35 (low — target 7.2–7.6) | +| ORP/Redox (LRPS) | `6480da07c2c0896da120ecce` | 646 | 646 mV | +| Filter pressure (LRPR) | `6a17218be0b88ea3e9255251` | 956 | — | +| Pool level switch | `6a174dd1e0b88ea3e925531e` | 1 | OK (1=full) | +| Fill flow volume | `6a1c4b153d6c33ab843edc15` | 0 | 0 L (no active fill) | +| Fill valve state | `6a1c4b153d6c33ab843edc16` | 1 | Closed | diff --git a/api-docs/overview.md b/api-docs/overview.md new file mode 100644 index 0000000..6df1467 --- /dev/null +++ b/api-docs/overview.md @@ -0,0 +1,139 @@ +# MySolem / MyIndygo API — Overview + +| Platform | Base URL | Focus | +|---|---|---| +| MySOLEM | `https://qualif.mysolem.com` | Irrigation, garden sensors | +| MyIndygo | `https://qualif.myindygo.com` | Pool control, water chemistry | + +Both platforms share **the same backend database and API** — same module IDs, input IDs, session cookie. MyIndygo adds pool-specific management endpoints on top. +See `api-docs/myindygo.md` for MyIndygo-specific docs. + +**Backend**: Node.js + Sails.js v0.13.8 socket client, MongoDB (all IDs are 24-char hex ObjectIds) +**Real-time**: Socket.IO v3 / Engine.IO v3 (`EIO=3`), direct WebSocket transport (not long-polling), `wss://qualif.mysolem.com/socket.io/` +**Proxy**: nginx/1.18.0 (Ubuntu) in front + +--- + +## Authentication + +Two separate auth systems exist on the same server: + +- **Web API** (no path prefix): session cookie (Sails.js), no CSRF required on JSON endpoints +- **Mobile API** (`/api/` prefix): OAuth2 Bearer token (`grant_type: client_credentials`) + +See `api-docs/mobile-api.md` for the mobile API auth flow. + +### Web login flow + +```bash +# Step 1: initialize cookie jar +curl -c cookies.txt https://qualif.mysolem.com/login + +# Step 2: authenticate +curl -c cookies.txt -b cookies.txt \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "email=YOUR_EMAIL&password=YOUR_PASSWORD" \ + "https://qualif.mysolem.com/login" + +# All subsequent calls +curl -b cookies.txt https://qualif.mysolem.com/features +``` + +Cookie name: `qualification-mysolem-solem-irrigation-platform.sid` +Session TTL: ~7 days +No CSRF token required on JSON endpoints. + +### Python example + +```python +import requests + +BASE = "https://qualif.mysolem.com" +s = requests.Session() + +s.get(f"{BASE}/login") # init cookie +s.post(f"{BASE}/login", data={ + "email": "...", + "password": "...", +}) # 302 → / on success + +# Now s carries the session cookie automatically +sites = s.get(f"{BASE}/canopy-sites", params={"userId": USER_ID}).json() +``` + +--- + +## Key IDs (test account) + +| Resource | Name | ID | Serial Number | +|---|---|---|---| +| User (MySOLEM) | Arnaud Nelissen | `5de53bca7ed4c45c2cfe067f` | — | +| User (MyIndygo) | Arnaud Nelissen (API account) | `6a21c3db5ebe0fa5f7885460` | — | +| Site | Domicile | `62b30d26ecbceb08607592c1` | — | +| Gateway module | LRMB10-070532 | `60ca4276ad69dac1558f7c61` | `1000000705320001` | +| Sensor station module | LRMS-0721DC | `60ca42adad69dac1558f7c64` | `7400040721DC0001` | +| Agricultural valve module | LR6AG-070917 | `626984f038ec558c598bb6d5` | `6606010709170001` | +| Pool analyser module | LRPS-0565C0 | `6480da07c2c0896da120ecca` | `9400030565C00001` | +| Pool ctrl module | LRPC-F1DA27 | `6481bcea8782b78554c02bed` | `150302F1DA270001` | +| IP ECO module | LR2IPECO-0C9282 | `6516ca57b7c098e6807cb332` | `3802020C92820001` | +| Filter pressure sensor | LRPR-0D6800 | `6a17218be0b88ea3e925524f` | `1D00010D68000001` | +| Pool level switch | POOL-LVL-0E953B | `6a174dd1e0b88ea3e925531c` | `0300020E953B0001` | +| Level ctrl module | LRLEVEL-0D2ED9 | `6a1c4b153d6c33ab843edc13` | `3A01020D2ED90001` | +| Input: Soil moisture (Plumbago) | — | `60ca42adad69dac1558f7c66` | +| Input: Sensor 2 | — | `60ca42adad69dac1558f7c67` | +| Input: Sensor 3 | — | `60ca42adad69dac1558f7c69` | +| Input: Rain sensor (AG) | — | `626984f038ec558c598bb6d7` | +| Input: Pool temperature (LRPS) | — | `6480da07c2c0896da120eccc` | +| Input: Pool pH (LRPS) | — | `6480da07c2c0896da120eccd` | +| Input: Pool ORP/Redox (LRPS) | — | `6480da07c2c0896da120ecce` | +| Input: Rain sensor (LRAG) | — | `626984f038ec558c598bb6d7` | +| Input: Moisture — Plumbago (LRMS) | — | `60ca42adad69dac1558f7c66` | +| Input: Moisture — Pelouse (LRMS) | — | `60ca42adad69dac1558f7c67` | +| Input: Temperature (LRMS) | — | `60ca42adad69dac1558f7c69` | +| Input: Flow meter (LRIPECO) | — | `6516ca57b7c098e6807cb334` | +| Input: Filter pressure (LRPR) | — | `6a17218be0b88ea3e9255251` | +| Input: Pool level switch (POOL-LVL) | — | `6a174dd1e0b88ea3e925531e` | +| Input: Pool level switch aux (POOL-LVL) | — | `6a174dd1e0b88ea3e925531f` | +| Input: Fill volume (LRLEVEL) | — | `6a1c4b153d6c33ab843edc15` | +| Input: Fill valve state (LRLEVEL) | — | `6a1c4b153d6c33ab843edc16` | +| Output: Station 1 (AG) | Station 1 | `626984f038ec558c598bb6d8` | +| Output: Station 2 (AG) | Station 2 | `626984f038ec558c598bb6d9` | + +--- + +## Module types + +| Type string | Description | +|---|---| +| `lr-mb-10` | LoRa relay / gateway | +| `lr-ms` | LoRa sensor station | +| `lr-ag` | LoRa agricultural valve | +| `lr-mas` | LoRa master valve | +| `lr-pc` | LoRa pool controller | +| `lr-ip-eco` | LoRa IP ECO | +| `lr-pr` | LoRa pressure sensor (filter) | +| `pool-level-sensor` | Pool float level switch | +| `lr-niv` | LoRa level controller (fill valve) | +| `wf-mb` | WiFi gateway | +| `wf-is` / `bl-is` | WiFi/BT sensor | +| `bl-ip` | BT IP | +| `wf-ol` | WiFi online | + +--- + +## Input type codes + +| Code | Meaning | Unit | +|---|---|---| +| `2` | TOR (on/off, e.g. rain sensor) | — | +| `5` | Air/soil temperature | °C | +| `6` | pH | — (raw ÷ 100) | +| `7` | ORP / Redox | mV | +| `8` | Pressure | bar (unit `6`, expression: `(1.5×raw−500)/1000`) | +| `12` | Soil moisture | % (unit `10`) | +| `20` | Air moisture | — | +| `21` | Pool level TOR switch | raw 0/1 | +| `33` | Volumetric / flow | L (unit `1`) | +| `133` | Water temperature | °C (unit `2`) | +| `149` | Fill valve / level state | raw 0/1 | diff --git a/api-docs/pool-level-sensor.md b/api-docs/pool-level-sensor.md new file mode 100644 index 0000000..1d83a58 --- /dev/null +++ b/api-docs/pool-level-sensor.md @@ -0,0 +1,53 @@ +# pool-level-sensor — Pool Level Float Switch (POOL-LVL-0E953B) + +**Module ID**: `6a174dd1e0b88ea3e925531c` +**Serial**: `0300020E953B0001` +**MAC**: `C8:B9:61:0E:95:3B` +**UUID**: `030058B9610E953B00000001000E953B` +**Firmware**: 7.3.1 | **Hardware**: B | **Manufacturer**: `03` +**Type**: `pool-level-sensor` +**Role**: Detects pool water level — triggers fill valve via LRLEVEL (lr-niv) + +--- + +## Inputs + +| Input ID | Index | Type | Unit | Interval | Notes | +|---|---|---|---|---|---| +| `6a174dd1e0b88ea3e925531e` | 1 | 21 (level TOR) | raw (0/1) | 1 min | Main level switch | +| `6a174dd1e0b88ea3e925531f` | 2 | 0 | raw | 0 (event-driven) | Secondary / aux (no telemetry recorded) | + +Both inputs: expression `x` (raw passthrough). `metadata.equipmentType: "waterLevel"` on input 1. + +### Recent readings + +``` +2026-05-31T14:54 1 (level OK / above threshold) +2026-05-30T18:27 1 (level OK) +2026-05-29T16:18 0 (level LOW — fill triggered) +2026-05-29T16:17 1 +2026-05-29T15:59 0 (level LOW) +``` + +`0` = water below threshold (fill needed) +`1` = water level OK + +Only 11 events recorded over the last month — sensor only transmits on state change. + +--- + +## Remote state + +- Battery: **41%** — low, needs replacement +- Last radio contact: 2026-06-04T20:14:07Z +- Relay: `60ca4276ad69dac1558f7c61` (LRMB10) + +The `poolLevelSensorCleaningReminder` alert (2026-05-28) confirms this device needs cleaning. + +--- + +## Notes + +- Works in tandem with `LRLEVEL-0D2ED9` (lr-niv): when this sensor reads 0, LRLEVEL opens the fill valve +- No programs or outputs — pure sensor +- Type 21 = pool level TOR switch (new type, not seen in other modules) diff --git a/api-docs/security-critique.md b/api-docs/security-critique.md new file mode 100644 index 0000000..f041fcf --- /dev/null +++ b/api-docs/security-critique.md @@ -0,0 +1,172 @@ +# MySolem API — Security Critique + +Assessment based on black-box reverse engineering of `https://qualif.mysolem.com`. + +--- + +## Critical + +### 1. `/modules/search` leaks the entire platform's device database + +`POST /modules/search` with an empty query (`"query": {}`) returned **4,607 modules** belonging to all users on the platform. Confirmed exposed fields: `name`, `serialNumber`, `macAddress`, `softwareVersion`, `seenAt` (last-seen timestamp), `batteryAlarm`, `uuid`, `type`, and `createdAt`. + +Any authenticated user can enumerate every device on the platform. This is a broken object-level authorization (BOLA / IDOR) vulnerability — the endpoint enforces authentication but not ownership scoping. + +**Impact**: full device inventory of all platform customers — device names, serial numbers, MAC addresses, firmware versions, and last-seen timestamps across all tenants. + +--- + +### 2. `POST /users/search` exposes full user records including password hashes and account takeover tokens + +`POST /users/search` accepts an arbitrary MongoDB-style query with no ownership enforcement. Querying by any known user ID returns the complete user document, including: + +- Email address and full name +- **Hashed password** (`password` field — 64-character hex string, consistent with unsalted SHA-256; notably not bcrypt which is Sails.js's default — warrants further analysis) +- **Active password reset token** (`resetPasswordToken`) — can be used directly to take over the account without knowing the current password +- **APNs and FCM push notification tokens** — allows sending arbitrary push notifications to the user's devices +- Address fields, notification preferences, and app version history + +User IDs are trivially obtained from finding #1: every module document in the `/modules/search` dump contains the owning user's ID in its relational fields. + +**Proof of concept**: querying `{"query": {"id": "641c3250efed32199bed0166"}}` returned the full record for `jdheilly@solem.fr` including password hash, reset token, and 11 push tokens across iOS and Android. + +**Impact**: complete account takeover of any platform user via the exposed reset token, plus credential exposure (unsalted SHA-256 hashes are trivially crackable). Push token exposure enables notification spam or phishing. This is the most severe finding in this assessment. + +--- + +### 3. `POST /outputs/search` and `POST /programs/search` expose full platform watering configuration + +Both search endpoints accept an empty MongoDB query with no ownership enforcement: + +- `POST /outputs/search` with `{"query": {}}` returned **12,034 outputs** (valve/station definitions) across all tenants, including names, flow rates, and module associations. +- `POST /programs/search` with `{"query": {}}` returned **6,073 irrigation programs** across all tenants, including full schedules, station durations, start times, water-budget percentages, and weekday patterns. + +**Impact**: complete read access to every customer's watering configuration. Combined with finding #4 (device control), this gives an attacker enough information to silently modify or disrupt any installation on the platform. + +--- + +### 4. `GET /remote/module/configuration` exposes device internals for any module + +The endpoint accepts any `moduleId` without ownership verification. Probing a foreign active module (`67697cc9673ef4bfbb4fdbbc`) returned full device configuration including: + +- WiFi SSID and all MAC addresses (WiFi, Bluetooth, Ethernet) +- Firmware, hardware, and BLE versions +- Connected network domain and IoT subdomain +- Uptime counter and reset count +- Active link states (WiFi, Bluetooth, modem) + +**Impact**: network topology and credential-adjacent data (WiFi SSID + MAC) for any online device on the platform, without owning it. + +--- + +### 5. No authorization check on `/remote/input/getComputedSensorData` + +The sensor telemetry endpoint accepts any `inputId`. If an attacker enumerates input IDs (trivially derived from the module search above — they are sequential MongoDB ObjectIds), they can pull historical sensor data for any device on the platform without owning it. + +**Impact**: silent exfiltration of long-term environmental data (soil moisture trends, rain events, watering schedules) for any customer site. + +--- + +### 7. `POST /module/sendManualModuleCommand` — no apparent ownership check + +The control endpoint takes a `moduleSerialNumber`, not a session-scoped module ID. Serial numbers are visible in the `/modules/search` dump. It is likely (not confirmed) that a valid session can send commands to modules owned by other users. + +**Impact**: if confirmed, any authenticated user could start or stop irrigation on any device on the platform — including commercial agricultural installations. + +--- + +## High + +### 8. Google Maps API key hard-coded in HTML + +The key `AIzaSyDh8fCC9vTEuN9eKewxgqWXGP9ENb5an1c` is embedded in every page load with no referrer restriction visible. It can be extracted and abused for Maps API billing fraud against the platform owner. + +--- + +### 9. Session cookie has no observed expiry enforcement beyond 7 days + +Sails.js default session TTL. There is no `SameSite` attribute confirmed in the `Set-Cookie` header, which means the cookie is sent on cross-site navigated requests — making CSRF possible for state-changing endpoints despite the lack of a CSRF token check (both conditions compound each other). + +A `SameSite=Lax` or `SameSite=Strict` attribute would mitigate this without requiring a token. + +--- + +### 10. No rate limiting observed on `/login` + +The login endpoint accepted repeated requests without delay, lockout, or CAPTCHA. Credential stuffing and brute-force attacks are uninhibited. + +--- + +### 11. MongoDB ObjectIds as primary keys are sequentially guessable + +MongoDB ObjectIds encode a timestamp + machine ID + process ID + counter. They are not random. An attacker who knows one valid ID can narrow the search space for adjacent resources created around the same time. Combined with findings #1–2, this accelerates enumeration significantly. + +--- + +## Medium + +### 12. CORS is misconfigured (blank `Access-Control-Allow-Origin`) + +The server returns `Access-Control-Allow-Origin: ` (empty string) rather than omitting the header or setting a specific origin. This is a misconfiguration — browsers treat it as invalid, but it signals the header is being generated dynamically and may be inconsistently applied across routes. + +--- + +### 13. No `Content-Security-Policy` header + +The React frontend loads without a CSP. If an XSS vulnerability exists anywhere in the app, the attacker has full access to the DOM and cookie-less session (cookies are `HttpOnly`, but XSS can still make authenticated API calls). + +--- + +### 14. Verbose server fingerprinting + +`X-Powered-By: Sails ` is returned on every response. Attackers can immediately target known Sails.js CVEs without fingerprinting effort. This header should be removed. + +--- + +### 15. Module serial numbers and MAC addresses exposed in bulk + +The `/modules/search` dump includes full `serialNumber`, `uuid`, `macAddress`, and `manufacturerType` for every device. These are used as authentication tokens in some IoT protocols (LoRa device EUIs, BLE MAC-based pairing). Bulk exposure reduces the bar for physical-layer attacks. + +--- + +## Low / Informational + +### 16. Qualification environment uses real production data + +The `qualif.mysolem.com` environment appears to share the same database as production (or a live mirror), given that real device GPS coordinates, real customer addresses, and live telemetry timestamps are visible. Qualification/staging environments should use anonymized or synthetic data. + +--- + +### 17. Input IDs discoverable via HTML scraping + +Input IDs are embedded as `data-input-id` attributes in the module detail page. This is not a vulnerability on its own, but combined with #1 (module enumeration) and #2 (unrestricted telemetry), it completes the chain without requiring any guessing. + +--- + +## Summary table + +| # | Finding | Confirmed | Severity | OWASP / CWE | +|---|---|---|---|---| +| 1 | `/modules/search` — 4,607 devices platform-wide | Yes | Critical | API1:2023 BOLA | +| 2 | `/users/search` — password hashes, reset tokens, push tokens | Yes | Critical | API1:2023 BOLA / CWE-312 | +| 3 | `/outputs/search` + `/programs/search` — 12K outputs, 6K programs | Yes | Critical | API1:2023 BOLA | +| 4 | `/remote/module/configuration` — WiFi/MAC/firmware for any device | Yes | Critical | API1:2023 BOLA | +| 5 | Sensor telemetry readable for any input ID | Likely | Critical | API1:2023 BOLA | +| 6 | Sensor history readable for any module ID | Likely | High | API1:2023 BOLA | +| 7 | Device control likely has no ownership check | Unconfirmed | Critical | API1:2023 BOLA | +| 8 | Google Maps API key in HTML, no restriction | Yes | High | CWE-312 | +| 9 | Session cookie missing `SameSite` attribute | Yes | High | CWE-352 CSRF | +| 10 | No login rate limiting | Yes | High | CWE-307 | +| 11 | Sequential ObjectIds accelerate enumeration | Yes | High | CWE-330 | +| 12 | Blank CORS header (misconfigured) | Yes | Medium | CWE-942 | +| 13 | No Content-Security-Policy | Yes | Medium | CWE-1021 | +| 14 | `X-Powered-By` header leaks framework | Yes | Low | CWE-200 | +| 15 | MAC/serial bulk exposure | Yes | Medium | CWE-200 | +| 16 | Staging uses real data | Yes | Medium | — | +| 17 | Input IDs in HTML source | Yes | Info | — | + +--- + +## Responsible disclosure note + +These findings were discovered through authorized testing of an account owned by the researcher. The critical BOLA issues (#1–7) should be reported to the MySolem security team before any public disclosure, as they affect all platform customers. diff --git a/api-docs/write-endpoints.md b/api-docs/write-endpoints.md new file mode 100644 index 0000000..b10ff1c --- /dev/null +++ b/api-docs/write-endpoints.md @@ -0,0 +1,249 @@ +# Write-Method Endpoints (POST / PUT / PATCH / DELETE) + +All endpoints that mutate state, sorted by risk. Sources: +- Static analysis of `/min/production.min.js` (8MB) and `/linker/js/esbuild-bundle.js` (16MB) +- Socket.io call extraction from both bundles +- `inputs/lrpc_post.json` Safari capture (real POST payloads) + +**Do not call destructive or control endpoints without intent.** Safe read-only probing stays on GET. + +--- + +## Device control (high impact — sends LoRa radio commands) + +### Pool controller impulse / start / stop +``` +POST /remote/module/control +Content-Type: application/json + +{ + "moduleSerialNumber": "150302F1DA270001", + "linesControl": [ + { "index": 0, "action": 0 } // stop output + { "index": 1, "action": 1 } // start (indefinitely) + { "index": 1, "action": 2, "time": "30:00" } // timed impulse MM:SS + { "index": 0, "action": 3, "time": "01:00" } // boost filtration + { "index": 0, "action": 5, "manualDuration": 1 } // pause (?) + { "index": 0, "action": 11, "time": "...", "speed": 1 } // backwash + ] +} +``` +Verified real payloads (mitmproxy captures): +- `{"action":0,"index":0}` → stop fill valve (LRLEVEL serial `3A01020D2ED90001`) +- `{"action":1,"index":1}` → Station 2, start indefinitely +- `{"action":2,"time":"30:00","index":1}` → Station 2, 30 min impulse +- `{"action":3,"time":"01:00","index":0}` → boost filtration 1 min (LRPC serial `150302F1DA270001`) + +### Irrigation valve start/stop (form-encoded — confirmed via mitmproxy) +``` +POST /module/sendManualModuleCommand +Content-Type: application/x-www-form-urlencoded +``` + +Confirmed payload patterns: + +| Intent | Params | +|---|---| +| Run station N minutes | `commandType=station&command={outputId}&commandParams[hours]=0&commandParams[minutes]=N` | +| Cycling station (on/off) | `commandType=station&command={outputId}&commandParams[hours]=1&commandParams[minutes]=0&commandParams[on]=1200&commandParams[off]=930&agriculturalModuleId={moduleId}` | +| Stop station | `commandType=station&command={outputId}&commandParams=stop&agriculturalModuleId={moduleId}` | +| All stations N minutes | `commandType=station&command=allStations&commandParams[hours]=0&commandParams[minutes]=N` | +| Stop everything | `commandType=manualStop` | +| Enable (no rain delay) | `commandType=status&command=on&commandParams=0` | +| Disable | `commandType=status&command=off&commandParams=0` | +| Rain delay N days | `commandType=status&command=off&commandParams=N` | +| Max rain delay | `commandType=status&command=off&commandParams=255` | +| Run program | `commandType=program&command={programId}` | + +Notes: +- `command` accepts an **output ID string**, `"allStations"`, or a **program ID** (not an action name). +- `agriculturalModuleId` is required for stop and cycling station commands. +- `commandType=manualStop` takes no `outputIndex` or `commandParams`. + +### Flush commands to device +``` +POST /canopy/{siteId}/sendModulesCommand {} +``` + +### Module LoRa data push (forces sync) +``` +POST /modules/sendDataViaLoRaWAN +POST /agriculturalModuleGroup/sendDataViaLoRaWAN +POST /wateringModuleGroup/sendDataViaLoRaWAN +``` + +--- + +## Program / schedule changes (persistent, survives reboot) + +``` +PUT /programs { "programs": [...], "module": "moduleId" } +POST /programs create new program +DELETE /programs delete program +PATCH /programs/name rename +POST /program/update socket.io variant (immediate push to device) +PUT /program/update REST variant +POST /program/reportProgramsDataSent acknowledge sync +POST /checkAvailableWindows check time conflicts before saving +``` + +Program window format for `PUT /programs`: +```json +{ + "programs": [{ + "id": "...", + "module": "moduleId", + "index": 0, + "weekDays": 127, + "waterBudget": 100, + "windows": [{ "start": 300, "end": 360, "on": 3600, "off": 0, "runningDays": 127 }], + "programCharacteristics": { "programType": 4, "mode": 2, "onSpeed": 1 } + }], + "module": "moduleId" +} +``` + +--- + +## Module configuration (persistent device settings) + +``` +POST /remote/module/configuration socket.io: update device config +POST /remote/module/configuration/and/programs config + programs in one push +POST /input/update update input settings (name, thresholds) +PUT /output/update update output settings (name, flowrate) +POST /remote/module/name rename via LoRa +PUT /module/update update module metadata +PUT /module/update/address +PUT /module/update/status +POST /module/update/name +POST /module/update/securityMode +POST /module/reportModuleDataSent acknowledge device sync +POST /remote/module/advertisingProfile BLE advertising config +POST /remote/modules/advertisingProfile bulk +POST /remote/module/dissociate { relayMsn, modulesMsn } — removes pairing +PUT /module/dissociate/{moduleId} +``` + +--- + +## Site / pool management + +``` +POST /canopy-sites create new site +PUT /pools/update/address update pool address +POST /pools/admin/sendMessage send admin message to pool customer +PUT /teams/handleAccessGrantedPools grant pool access to team +POST /teams/members/add +POST /teams/members/create +POST /agency/team/members/add +POST /addAgriculturalGroupToUser +POST /wateringModuleGroup/create +PUT /wateringModuleGroup/update +PUT /wateringModuleGroup/update/name +PUT /wateringModuleGroup/updateFlowRate +DELETE /wateringModuleGroup/delete +POST /wateringModuleGroup/remove +POST /updateAgriculturalModuleGroup +``` + +--- + +## Journal / notifications + +``` +PUT /journal/acknowledge/all +PUT /journal/acknowledge/records +POST /canopy-sites/{siteId}/notifications/acknowledge-all +POST /journal/notifications (write variant — mark read?) +POST /journal/counts +POST /journal/warnings/accepted +``` + +--- + +## Content / documents (safe-ish) + +``` +PUT /favorites +PUT /favorites/ +POST /module/tags { module, tags: [...] } +DELETE /document/remove +DELETE /canopy/v2/deleteImage +POST /hpaform/create +POST /library/program-template +POST /library/programming-template +POST /notices/remove +POST /polytropic/setIpxData +POST /updateOutputIpxData +``` + +--- + +## Admin-only (requires admin role — will 403 on standard account) + +``` +POST /admin/modules/search +DELETE /admin/sim-card/delete +POST /admin/sim-card/update +POST /manufacturer/upload-blacklist +POST /firmwares/removeModuleFirmware +POST /pools/admin/sendMessage +POST /pools/admin/search +``` + +--- + +## How to discover more (without executing) + +Three complementary techniques, all read-only: + +**1. Static JS analysis** (already done above): +```bash +# All $.ajax write calls +grep -oE 'type:"(POST|PUT|PATCH|DELETE)"[^}]{0,200}url:"[^"]*"' /tmp/indygo_prod.js + +# All fetch() write calls +grep -oE 'fetch\("[^"]*"[^)]{0,200}method:"(POST|PUT|PATCH|DELETE)"' /tmp/indygo_prod.js + +# Socket.io write calls +grep -oE 'io\.socket\.(post|put|patch|delete)\("[^"]*"' /tmp/indygo_prod.js +``` + +**2. Safari Web Inspector recording** — record a session where you perform the action in the real app, then parse `inputs/*.json`: +```python +import json +with open("inputs/lrpc_post.json") as f: + data = json.load(f) +records = data["recording"]["records"] +for rec in records: + req = rec.get("entry", {}).get("request", {}) + if req.get("method") in ("POST","PUT","PATCH","DELETE"): + print(req["method"], req["url"]) + print(" ", req.get("postData", {}).get("text", "")[:200]) +``` + +**3. Mitmproxy / Charles** (more complete than Safari — captures socket.io frames): +```bash +mitmproxy --mode transparent --ssl-insecure -s dump_writes.py +# dump_writes.py: filter on method != GET, log url + body +``` +Safari doesn't capture WebSocket frames (socket.io messages), so LoRa push commands that go via socket.io are invisible in recordings. + +--- + +## Socket.io write calls (discovered via bundle grep) + +These bypass the normal HTTP layer — standard Safari recording captures neither the URL nor body: +``` +socket.post /checkAvailableWindows +socket.post /input/update +socket.post /program/update +socket.post /remote/module/configuration +socket.post /remote/module/configuration/and/programs +socket.post /remote/module/name +socket.put /module/update +socket.put /module/update/address +socket.put /pools/update/address +socket.put /program/update +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..00de492 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + myprimal: + build: . + restart: unless-stopped + ports: + - "${PORT:-3001}:3001" + env_file: .env + volumes: + - myprimal-config:/app/config.json + +volumes: + myprimal-config: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9d968f2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6438 @@ +{ + "name": "myprimal", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "myprimal", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.0", + "axios-cookiejar-support": "^4.0.0", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "mqtt": "^5.0.0", + "tough-cookie": "^4.1.4" + }, + "devDependencies": { + "jest": "^30.4.2", + "nodemon": "^3.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.4.1", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios-cookiejar-support": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-4.0.7.tgz", + "integrity": "sha512-9vpE3y/a2l2Vs2XEJE4L2z0GWnlpJ4Xj+kDaoCtrpPfS1J3oikXBrxRJX6H62/ZcelOGe+519yW7mqXCIoPXuw==", + "license": "MIT", + "dependencies": { + "http-cookie-agent": "^5.0.4" + }, + "engines": { + "node": ">=14.18.0 <15.0.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/3846masa" + }, + "peerDependencies": { + "axios": ">=0.20.0", + "tough-cookie": ">=4.0.0" + } + }, + "node_modules/babel-jest": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.4.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.4.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.4.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/broker-factory": { + "version": "3.1.15", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz", + "integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.380", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz", + "integrity": "sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cookie-agent": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-5.0.4.tgz", + "integrity": "sha512-OtvikW69RvfyP6Lsequ0fN5R49S+8QcS9zwd58k6VSr6r57T8G29BkPdyrBcSwLq6ExLs9V+rBlfxu7gDstJag==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0" + }, + "engines": { + "node": ">=14.18.0 <15.0.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/3846masa" + }, + "peerDependencies": { + "deasync": "^0.1.26", + "tough-cookie": "^4.0.0", + "undici": "^5.11.0" + }, + "peerDependenciesMeta": { + "deasync": { + "optional": true + }, + "undici": { + "optional": true + } + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", + "import-local": "^3.2.0", + "jest-cli": "30.4.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0", + "pretty-format": "30.4.1", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "parse-json": "^5.2.0", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "jest-util": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-haste-map/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.4.1", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mqtt": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz", + "integrity": "sha512-V1WnkGuJh3ec9QXzy5Iylw8OOBK+Xu1WhxcQ9mMpLThG+/JZIMV1PgLNRgIiqXhZnvnVLsuyxHl5A/3bHHbcAA==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt-packet/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mqtt/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/number-allocator/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/number-allocator/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/worker-factory": { + "version": "7.0.50", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz", + "integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.33", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.33.tgz", + "integrity": "sha512-RQVlKkek80v8M6SHvdMKiywaXFmX3XTpYTXTw0r62PdXB06t74XNxcTuG8wsQwnjorAQymNQyyQ0rzv7PykEMQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.18", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.18", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz", + "integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "broker-factory": "^3.1.15", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz", + "integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..718df72 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "myprimal", + "version": "1.0.0", + "description": "MySolem/MyIndygo IoT bridge — MQTT + Home Assistant autodiscovery", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "test": "jest" + }, + "dependencies": { + "axios": "^1.7.0", + "axios-cookiejar-support": "^4.0.0", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "mqtt": "^5.0.0", + "tough-cookie": "^4.1.4" + }, + "devDependencies": { + "jest": "^30.4.2", + "nodemon": "^3.1.0" + } +} diff --git a/src/api/routes.js b/src/api/routes.js new file mode 100644 index 0000000..013782a --- /dev/null +++ b/src/api/routes.js @@ -0,0 +1,61 @@ +'use strict'; +const express = require('express'); +const router = express.Router(); +const { getRegistry, allInputs, allOutputs } = require('../registry'); +const state = require('../state'); +const { sendCommand } = require('../control'); +const { discoverAll } = require('../registry'); +const { publishAll } = require('../mqtt/discovery'); +const poller = require('../poller'); + +router.get('/health', (req, res) => { + const reg = getRegistry(); + res.json({ + status: 'ok', + modules: reg.size, + inputs: state.inputCount(), + uptime: Math.floor(process.uptime()), + }); +}); + +router.get('/state', (req, res) => { + res.json(state.getAll()); +}); + +router.get('/state/:moduleId', (req, res) => { + const data = state.getByModule(req.params.moduleId); + if (Object.keys(data).length === 0) return res.status(404).json({ error: 'Module not found or no data' }); + res.json(data); +}); + +router.get('/registry', (req, res) => { + const result = {}; + for (const [id, m] of getRegistry()) result[id] = m; + res.json(result); +}); + +router.post('/control/:moduleId/:outputIndex', async (req, res) => { + const { moduleId, outputIndex } = req.params; + const cmd = req.body; + if (!cmd || !cmd.action) return res.status(400).json({ error: 'body.action required' }); + try { + const result = await sendCommand(moduleId, parseInt(outputIndex, 10), cmd); + res.json({ ok: true, result }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +router.post('/rediscover', async (req, res) => { + try { + poller.stop(); + await discoverAll(); + poller.start(); + try { publishAll(); } catch (_) {} + res.json({ ok: true, modules: getRegistry().size }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +module.exports = router; diff --git a/src/api/server.js b/src/api/server.js new file mode 100644 index 0000000..b7bc403 --- /dev/null +++ b/src/api/server.js @@ -0,0 +1,17 @@ +'use strict'; +const express = require('express'); + +const app = express(); +app.use(express.json()); +app.use('/', require('./routes')); + +function listen(port) { + return new Promise(resolve => { + const server = app.listen(port, () => { + console.log(`[api] Listening on http://localhost:${port}`); + resolve(server); + }); + }); +} + +module.exports = { listen }; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..1aae6c9 --- /dev/null +++ b/src/config.js @@ -0,0 +1,57 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const { EventEmitter } = require('events'); + +const CONFIG_PATH = process.env.MYPRIMAL_CONFIG_PATH || path.join(__dirname, '..', 'config.json'); + +const DEFAULTS = { + poll: { + fast: parseInt(process.env.POLL_FAST) || 60, + normal: parseInt(process.env.POLL_NORMAL) || 300, + slow: parseInt(process.env.POLL_SLOW) || 900, + overrides: {}, + }, + expressions: {}, // inputId → expression string override, e.g. "value/10" +}; + +const emitter = new EventEmitter(); +let _config = deepMerge({}, DEFAULTS); + +function deepMerge(target, source) { + for (const key of Object.keys(source || {})) { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + target[key] = deepMerge(target[key] || {}, source[key]); + } else { + target[key] = source[key]; + } + } + return target; +} + +function loadConfig() { + if (fs.existsSync(CONFIG_PATH)) { + try { + const saved = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); + _config = deepMerge(deepMerge({}, DEFAULTS), saved); + } catch (e) { + console.warn('[config] Failed to parse config.json, using defaults:', e.message); + } + } +} + +function saveConfig() { + fs.writeFileSync(CONFIG_PATH, JSON.stringify(_config, null, 2)); +} + +function getConfig() { + return _config; +} + +function patchConfig(patch) { + _config = deepMerge(_config, patch); + saveConfig(); + emitter.emit('change', _config); +} + +module.exports = { loadConfig, getConfig, patchConfig, on: emitter.on.bind(emitter) }; diff --git a/src/control.js b/src/control.js new file mode 100644 index 0000000..6643d79 --- /dev/null +++ b/src/control.js @@ -0,0 +1,93 @@ +'use strict'; +const { post, postForm, INDYGO_BASE } = require('./solem'); +const { getModule } = require('./registry'); +const { getProtocol } = require('./module-types'); + +// Duration in minutes → "HH:MM" string for linesControl +function minutesToHHMM(minutes) { + const h = Math.floor(minutes / 60); + const m = minutes % 60; + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; +} + +async function linesControl(module, outputIndex, cmd) { + const line = { index: outputIndex }; + + switch (cmd.action) { + case 'run': + line.action = 2; + line.time = minutesToHHMM(cmd.duration || 30); + break; + case 'stop': + line.action = 1; + break; + case 'boost': + line.action = 3; + line.time = minutesToHHMM(cmd.duration || 60); + break; + case 'pause': + line.action = 5; + line.manualDuration = cmd.days || 1; + break; + default: + throw new Error(`Unknown action "${cmd.action}" for linesControl`); + } + + return post('/remote/module/control', { + moduleSerialNumber: module.serial, + linesControl: [line], + }, INDYGO_BASE); +} + +async function manualCommand(module, outputIndex, cmd) { + const output = module.outputs.find(o => o.index === outputIndex); + const outputId = output?.id; + + switch (cmd.action) { + case 'run': { + const totalMinutes = cmd.duration || 30; + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return postForm('/module/sendManualModuleCommand', { + commandType: 'station', + command: outputId, + 'commandParams[hours]': String(hours), + 'commandParams[minutes]': String(minutes), + 'commandParams[on]': '0', + 'commandParams[off]': '0', + }); + } + case 'stop': + return postForm('/module/sendManualModuleCommand', { + commandType: 'station', + command: outputId, + commandParams: 'stop', + agriculturalModuleId: module.id, + }); + case 'pause': + return postForm('/module/sendManualModuleCommand', { + commandType: 'status', + command: 'off', + commandParams: String(cmd.days || 1), + }); + case 'boost': + console.warn(`[control] boost not supported for manualCommand protocol (module ${module.id})`); + throw new Error('boost not supported for this module type'); + default: + throw new Error(`Unknown action "${cmd.action}" for manualCommand`); + } +} + +async function sendCommand(moduleId, outputIndex, cmd) { + const module = getModule(moduleId); + if (!module) throw new Error(`Module ${moduleId} not found in registry`); + + const protocol = getProtocol(module.type); + + if (protocol === 'linesControl') return linesControl(module, outputIndex, cmd); + if (protocol === 'manualCommand') return manualCommand(module, outputIndex, cmd); + + throw new Error(`Unsupported protocol "${protocol}" for module ${moduleId}`); +} + +module.exports = { sendCommand }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..d37729c --- /dev/null +++ b/src/index.js @@ -0,0 +1,50 @@ +'use strict'; +require('dotenv').config(); + +const { loadConfig } = require('./config'); +const { init: solemInit } = require('./solem'); +const { discoverAll } = require('./registry'); +const poller = require('./poller'); +const mqttClient = require('./mqtt/client'); +const mqttPublish = require('./mqtt/publish'); +const mqttDiscovery = require('./mqtt/discovery'); +const mqttCommands = require('./mqtt/commands'); +const mqttConfigBridge = require('./mqtt/config-bridge'); +const api = require('./api/server'); + +async function main() { + // 1. Load persisted config + loadConfig(); + + // 2. Authenticate with MySolem / MyIndygo + await solemInit(); + + // 3. Discover all modules, inputs, outputs + await discoverAll(); + + // 4. Start polling — feeds state cache + poller.start(); + + // 5. Connect to MQTT broker + try { + await mqttClient.connect(); + + // Wire MQTT integrations on connect + mqttPublish.start(); // state updates → MQTT retained messages + mqttDiscovery.publishAll(); // HA autodiscovery + mqttCommands.subscribeAll();// /set topics → control API + mqttConfigBridge.subscribe();// $config/set → config patches + } catch (e) { + console.warn('[mqtt] Could not connect — MQTT bridge disabled:', e.message); + console.warn('[mqtt] Service will still run with REST API only.'); + } + + // 6. Start REST API + const port = parseInt(process.env.PORT) || 3001; + await api.listen(port); +} + +main().catch(err => { + console.error('Fatal:', err.message); + process.exit(1); +}); diff --git a/src/input-types.js b/src/input-types.js new file mode 100644 index 0000000..34a4430 --- /dev/null +++ b/src/input-types.js @@ -0,0 +1,23 @@ +'use strict'; + +const INPUT_TYPES = { + 2: { haComponent: 'binary_sensor', deviceClass: 'opening', unit: null, expression: null, pollBucket: 'slow' }, + 5: { haComponent: 'sensor', deviceClass: 'temperature', unit: '°C', expression: v => v / 100, pollBucket: 'normal' }, + 6: { haComponent: 'sensor', deviceClass: null, unit: 'pH', expression: v => v / 100, pollBucket: 'fast' }, + 7: { haComponent: 'sensor', deviceClass: null, unit: 'mV', expression: null, pollBucket: 'fast' }, + 8: { haComponent: 'sensor', deviceClass: 'pressure', unit: 'bar', expression: v => (1.5 * v - 500) / 1000, pollBucket: 'fast' }, + 12: { haComponent: 'sensor', deviceClass: 'humidity', unit: '%', expression: null, pollBucket: 'normal' }, + 20: { haComponent: 'sensor', deviceClass: 'humidity', unit: '%', expression: null, pollBucket: 'slow' }, + 21: { haComponent: 'binary_sensor', deviceClass: 'moisture', unit: null, expression: null, pollBucket: 'slow' }, + 33: { haComponent: 'sensor', deviceClass: null, unit: 'L', expression: null, pollBucket: 'normal' }, + 133: { haComponent: 'sensor', deviceClass: 'temperature', unit: '°C', expression: v => v / 100, pollBucket: 'fast' }, + 149: { haComponent: 'binary_sensor', deviceClass: 'opening', unit: null, expression: null, pollBucket: 'slow' }, +}; + +const FALLBACK = { haComponent: 'sensor', deviceClass: null, unit: null, expression: null, pollBucket: 'slow' }; + +function getInputType(typeCode) { + return INPUT_TYPES[typeCode] || { ...FALLBACK, _unknown: true }; +} + +module.exports = { getInputType }; diff --git a/src/module-types.js b/src/module-types.js new file mode 100644 index 0000000..9178f0f --- /dev/null +++ b/src/module-types.js @@ -0,0 +1,38 @@ +'use strict'; + +const PROTOCOLS = { + 'lr-pc': 'linesControl', + 'lr-pc-vs': 'linesControl', + 'lr-niv': 'linesControl', + 'lr-mv': 'linesControl', + 'lr-ol': 'linesControl', + 'lr-ag': 'manualCommand', + 'lr-ip-eco': 'manualCommand', + 'lr-ip-fl': 'manualCommand', + 'lr-ip-fl-v2': 'manualCommand', + 'lr-ip-wan-v2': 'manualCommand', + 'bl-ip': 'manualCommand', + 'bl-ip-v2': 'manualCommand', +}; + +function getProtocol(moduleType) { + if (PROTOCOLS[moduleType]) return PROTOCOLS[moduleType]; + console.warn(`[module-types] Unknown type "${moduleType}" — defaulting to linesControl`); + return 'linesControl'; +} + +function isSensorOnly(moduleType) { + const sensorTypes = new Set([ + 'lr-ms', 'lr-ms-eco', 'lr-ms-v2', 'lr-ms-rs485', + 'lr-mas', 'lr-pr', 'lr-is', 'lr-is-city', + 'pool-level-sensor', 'wf-is', 'bl-is', 'smart-is', 'smart-is-lcd', + 'joro', 'joro-v2', + ]); + return sensorTypes.has(moduleType); +} + +function isGateway(moduleType) { + return moduleType.startsWith('lr-mb') || moduleType === 'wf-mb'; +} + +module.exports = { getProtocol, isSensorOnly, isGateway }; diff --git a/src/mqtt/client.js b/src/mqtt/client.js new file mode 100644 index 0000000..ce6e444 --- /dev/null +++ b/src/mqtt/client.js @@ -0,0 +1,61 @@ +'use strict'; +const mqtt = require('mqtt'); + +const PREFIX = process.env.MQTT_TOPIC_PREFIX || 'myprimal'; + +let client = null; + +function connect() { + return new Promise((resolve, reject) => { + const url = process.env.MQTT_URL || 'mqtt://localhost:1883'; + const opts = { + clientId: `myprimal_${Math.random().toString(16).slice(2, 8)}`, + clean: true, + reconnectPeriod: 5000, + }; + if (process.env.MQTT_USERNAME) opts.username = process.env.MQTT_USERNAME; + if (process.env.MQTT_PASSWORD) opts.password = process.env.MQTT_PASSWORD; + + client = mqtt.connect(url, opts); + + client.once('connect', () => { + console.log(`[mqtt] Connected to ${url}`); + resolve(client); + }); + + client.once('error', err => { + console.error('[mqtt] Connection error:', err.message); + reject(err); + }); + + client.on('error', err => console.error('[mqtt] Error:', err.message)); + + client.on('reconnect', () => console.log('[mqtt] Reconnecting…')); + }); +} + +function publish(topic, payload, opts = {}) { + if (!client) return; + const msg = typeof payload === 'string' ? payload : JSON.stringify(payload); + client.publish(`${PREFIX}/${topic}`, msg, { retain: true, ...opts }); +} + +function subscribe(topic, handler) { + if (!client) return; + const full = `${PREFIX}/${topic}`; + client.subscribe(full); + client.on('message', (t, buf) => { + if (t !== full) return; + try { + handler(JSON.parse(buf.toString())); + } catch { + handler(buf.toString()); + } + }); +} + +function getPrefix() { return PREFIX; } + +function getClient() { return client; } + +module.exports = { connect, publish, subscribe, getPrefix, getClient }; diff --git a/src/mqtt/commands.js b/src/mqtt/commands.js new file mode 100644 index 0000000..d4dff0d --- /dev/null +++ b/src/mqtt/commands.js @@ -0,0 +1,56 @@ +'use strict'; +const { getClient, getPrefix, publish } = require('./client'); +const { allOutputs } = require('../registry'); +const { sendCommand } = require('../control'); + +function subscribeAll() { + const client = getClient(); + if (!client) return; + const prefix = getPrefix(); + + for (const out of allOutputs()) { + const topic = `${prefix}/${out.moduleSlug}/output/${out.index}/set`; + client.subscribe(topic); + } + + client.on('message', async (topic, buf) => { + const prefix_ = `${prefix}/`; + if (!topic.startsWith(prefix_)) return; + + // Match: {prefix}/{slug}/output/{index}/set + const parts = topic.slice(prefix_.length).split('/'); + if (parts.length !== 4 || parts[1] !== 'output' || parts[3] !== 'set') return; + + const [slug, , indexStr] = parts; + const outputIndex = parseInt(indexStr, 10); + + let cmd; + try { + cmd = JSON.parse(buf.toString()); + } catch { + console.warn(`[commands] Invalid JSON on ${topic}`); + return; + } + + // Find module by slug + const output = allOutputs().find(o => o.moduleSlug === slug && o.index === outputIndex); + if (!output) { + console.warn(`[commands] No output found for ${slug}/output/${outputIndex}`); + return; + } + + try { + await sendCommand(output.moduleId, outputIndex, cmd); + // Reflect running state optimistically + const stateValue = (cmd.action === 'run' || cmd.action === 'boost') ? 1 : 0; + publish(`${slug}/output/${outputIndex}/state`, { value: stateValue, ts: new Date().toISOString() }); + console.log(`[commands] ${slug}/output/${outputIndex} → ${cmd.action}`); + } catch (e) { + console.error(`[commands] Command failed for ${slug}/output/${outputIndex}:`, e.message); + } + }); + + console.log(`[commands] Subscribed to ${allOutputs().length} output set topics`); +} + +module.exports = { subscribeAll }; diff --git a/src/mqtt/config-bridge.js b/src/mqtt/config-bridge.js new file mode 100644 index 0000000..07304f6 --- /dev/null +++ b/src/mqtt/config-bridge.js @@ -0,0 +1,41 @@ +'use strict'; +const { getClient, getPrefix, publish } = require('./client'); +const { getConfig, patchConfig, on: onConfigChange } = require('../config'); +const poller = require('../poller'); + +function subscribe() { + const client = getClient(); + if (!client) return; + const prefix = getPrefix(); + + const setTopic = `${prefix}/$config/set`; + client.subscribe(setTopic); + + client.on('message', (topic, buf) => { + if (topic !== setTopic) return; + let patch; + try { + patch = JSON.parse(buf.toString()); + } catch { + console.warn('[config-bridge] Invalid JSON on $config/set'); + return; + } + patchConfig(patch); + console.log('[config-bridge] Config updated:', JSON.stringify(patch)); + }); + + // Republish current config on connect + publishConfig(); + + // Republish + reload poller when config changes + onConfigChange('change', () => { + publishConfig(); + poller.reload(); + }); +} + +function publishConfig() { + publish('$config', getConfig()); +} + +module.exports = { subscribe, publishConfig }; diff --git a/src/mqtt/discovery.js b/src/mqtt/discovery.js new file mode 100644 index 0000000..e7d2d30 --- /dev/null +++ b/src/mqtt/discovery.js @@ -0,0 +1,76 @@ +'use strict'; +const { getClient, getPrefix } = require('./client'); +const { allInputs, allOutputs } = require('../registry'); +const { getInputType } = require('../input-types'); + +function publishAll() { + const client = getClient(); + if (!client) return; + const prefix = getPrefix(); + + for (const inp of allInputs()) { + const typeDef = getInputType(inp.typeCode); + const stateTopic = `${prefix}/${inp.moduleSlug}/sensor/${inp.metric}`; + const availTopic = `${prefix}/${inp.moduleSlug}/availability`; + + const payload = { + name: inp.name || inp.metric, + unique_id: `myprimal_${inp.id}`, + state_topic: stateTopic, + value_template: '{{ value_json.value }}', + availability_topic: availTopic, + device: { + identifiers: [`myprimal_${inp.moduleId}`], + name: inp.moduleName, + manufacturer: 'Solem', + }, + }; + + if (typeDef.deviceClass) payload.device_class = typeDef.deviceClass; + if (typeDef.unit) payload.unit_of_measurement = typeDef.unit; + + const component = typeDef.haComponent; + const configTopic = `homeassistant/${component}/myprimal_${inp.id}/config`; + client.publish(configTopic, JSON.stringify(payload), { retain: true }); + } + + for (const out of allOutputs()) { + const stateTopic = `${prefix}/${out.moduleSlug}/output/${out.index}/state`; + const cmdTopic = `${prefix}/${out.moduleSlug}/output/${out.index}/set`; + const availTopic = `${prefix}/${out.moduleSlug}/availability`; + + const payload = { + name: out.name, + unique_id: `myprimal_${out.moduleId}_${out.index}`, + state_topic: stateTopic, + command_topic: cmdTopic, + payload_on: JSON.stringify({ action: 'run', duration: 30 }), + payload_off: JSON.stringify({ action: 'stop' }), + value_template: '{{ value_json.value }}', + state_on: '1', + state_off: '0', + availability_topic: availTopic, + device: { + identifiers: [`myprimal_${out.moduleId}`], + name: out.moduleName, + manufacturer: 'Solem', + }, + }; + + const configTopic = `homeassistant/switch/myprimal_${out.moduleId}_${out.index}/config`; + client.publish(configTopic, JSON.stringify(payload), { retain: true }); + } + + // Publish availability online for all modules + const seen = new Set(); + for (const inp of allInputs()) { + if (!seen.has(inp.moduleSlug)) { + seen.add(inp.moduleSlug); + getClient().publish(`${prefix}/${inp.moduleSlug}/availability`, 'online', { retain: true }); + } + } + + console.log('[discovery] HA autodiscovery published'); +} + +module.exports = { publishAll }; diff --git a/src/mqtt/publish.js b/src/mqtt/publish.js new file mode 100644 index 0000000..0481675 --- /dev/null +++ b/src/mqtt/publish.js @@ -0,0 +1,21 @@ +'use strict'; +const { publish } = require('./client'); +const state = require('../state'); + +function publishReading(inputId, entry) { + const { moduleSlug, metric, value, unit, timestamp } = entry; + publish(`${moduleSlug}/sensor/${metric}`, { value, unit, ts: timestamp }); +} + +function start() { + // Publish initial state from cache + const all = state.getAll(); + for (const [inputId, entry] of Object.entries(all)) { + publishReading(inputId, entry); + } + + // Publish on every new reading + state.on('update', publishReading); +} + +module.exports = { start }; diff --git a/src/normalize.js b/src/normalize.js new file mode 100644 index 0000000..52213cd --- /dev/null +++ b/src/normalize.js @@ -0,0 +1,49 @@ +'use strict'; +const { getInputType } = require('./input-types'); + +// Safely compile an expression string from the API (e.g. "value/100", "(1.5*value-500)/1000") +// Only accepts expressions using: value, numbers, +, -, *, /, (, ), spaces, dots +const SAFE_EXPR = /^[\d\s+\-*/().value]+$/; +const _compiled = new Map(); +function compileExpression(expr) { + if (!expr || typeof expr !== 'string') return null; + const cleaned = expr.trim(); + if (!SAFE_EXPR.test(cleaned)) return null; + if (_compiled.has(cleaned)) return _compiled.get(cleaned); + try { + // eslint-disable-next-line no-new-func + const fn = new Function('value', `return ${cleaned}`); + _compiled.set(cleaned, fn); + return fn; + } catch { + return null; + } +} + +function normalizeValue(rawValue, typeCode, apiExpression) { + if (rawValue == null) return null; + + // 1. API-provided expression string (per-input, most specific) + if (apiExpression) { + const fn = compileExpression(apiExpression); + if (fn) { + try { return fn(rawValue); } catch { /* fall through */ } + } + } + + // 2. Hardcoded type-level expression (input-types.js) + const typeDef = getInputType(typeCode); + if (typeDef.expression) return typeDef.expression(rawValue); + + // 3. Passthrough + return rawValue; +} + +function toMetricName(inputName, inputIndex) { + if (inputName && inputName.trim()) { + return inputName.trim().toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, ''); + } + return `input_${inputIndex}`; +} + +module.exports = { normalizeValue, toMetricName }; diff --git a/src/poller.js b/src/poller.js new file mode 100644 index 0000000..d829fe9 --- /dev/null +++ b/src/poller.js @@ -0,0 +1,109 @@ +'use strict'; +const { get, INDYGO_BASE } = require('./solem'); +const { allInputs, getRegistry } = require('./registry'); +const { normalizeValue } = require('./normalize'); +const { getConfig } = require('./config'); +const state = require('./state'); + +// Pool-related module types that must use INDYGO_BASE +const INDYGO_TYPES = new Set(['lr-pc', 'lr-pc-vs', 'lr-mas', 'lr-pr', 'lr-niv', 'pool-level-sensor']); + +const timers = {}; + +function getBase(moduleType) { + return INDYGO_TYPES.has(moduleType) ? INDYGO_BASE : undefined; +} + +async function pollInput(inp) { + try { + const r = await get('/input/getLastTickFromInput', { inputId: inp.id }, getBase(inp.moduleType)); + const raw = r?.tick?.value; + if (raw == null) return; + const cfgOverride = getConfig().expressions?.[inp.id]; + const value = normalizeValue(raw, inp.typeCode, cfgOverride ?? inp.expression); + state.set(inp.id, { + value, + rawValue: raw, + unit: inp.unit, + moduleId: inp.moduleId, + moduleSlug: inp.moduleSlug, + moduleName: inp.moduleName, + metric: inp.metric, + timestamp: r?.tick?.timestamp || new Date().toISOString(), + }); + } catch (e) { + // silent — individual input failures shouldn't crash the poller + } +} + +function getBucketInterval(bucket) { + const cfg = getConfig().poll; + return (cfg.overrides?.[bucket] ?? cfg[bucket] ?? 300) * 1000; +} + +function getOverrideInterval(key) { + const overrides = getConfig().poll.overrides || {}; + const ms = overrides[key]; + return ms != null ? ms * 1000 : null; +} + +function scheduleBucket(bucket, inputs) { + if (timers[bucket]) clearInterval(timers[bucket]); + if (inputs.length === 0) return; + + const poll = () => Promise.all(inputs.map(pollInput)); + poll(); // immediate first poll + + timers[bucket] = setInterval(poll, getBucketInterval(bucket)); +} + +function start() { + const inputs = allInputs(); + + // Attach moduleType to each input entry (needed for base URL selection) + const reg = getRegistry(); + for (const inp of inputs) { + const mod = reg.get(inp.moduleId); + inp.moduleType = mod?.type || ''; + } + + const byBucket = { fast: [], normal: [], slow: [] }; + for (const inp of inputs) { + const overrideKey = `${inp.moduleSlug}/${inp.metric}`; + // Per-input overrides use their own interval; bucket them as 'slow' placeholder + if (getOverrideInterval(overrideKey) != null) { + // Schedule individually + scheduleInput(inp); + } else { + const bucket = inp.pollBucket || 'slow'; + (byBucket[bucket] || byBucket.slow).push(inp); + } + } + + for (const [bucket, list] of Object.entries(byBucket)) { + scheduleBucket(bucket, list); + } + + console.log(`[poller] Started — fast:${byBucket.fast.length} normal:${byBucket.normal.length} slow:${byBucket.slow.length} inputs`); +} + +function scheduleInput(inp) { + const key = `input:${inp.id}`; + if (timers[key]) clearInterval(timers[key]); + const ms = getOverrideInterval(`${inp.moduleSlug}/${inp.metric}`) || getBucketInterval(inp.pollBucket); + const poll = () => pollInput(inp); + poll(); + timers[key] = setInterval(poll, ms); +} + +function stop() { + for (const t of Object.values(timers)) clearInterval(t); + Object.keys(timers).forEach(k => delete timers[k]); +} + +function reload() { + stop(); + start(); +} + +module.exports = { start, stop, reload }; diff --git a/src/registry.js b/src/registry.js new file mode 100644 index 0000000..2341b23 --- /dev/null +++ b/src/registry.js @@ -0,0 +1,159 @@ +'use strict'; +const { get, post } = require('./solem'); +const { getInputType } = require('./input-types'); +const { isGateway, isSensorOnly } = require('./module-types'); +const { toMetricName } = require('./normalize'); + +const USER_ID = process.env.SOLEM_USER_ID || '5de53bca7ed4c45c2cfe067f'; + +// Map +const registry = new Map(); + +function makeSlug(type, serial) { + const suffix = (serial || '').slice(-6).toLowerCase(); + return `${type}_${suffix}`.replace(/[^a-z0-9_]/g, '_'); +} + +async function fetchInputs(moduleId) { + // Primary endpoint + try { + const data = await get(`/input/getInputsLinkedModule/${moduleId}`); + const arr = Array.isArray(data) ? data : (data?.inputs || data?.result || data?.data || data?.items || []); + if (arr.length > 0) return arr; + } catch { /* fall through */ } + + // Fallback: getComputedSensorsData returns full input objects including id/name/type + // Use a 24h window — enough to discover inputs even on low-frequency sensors + try { + const endDate = new Date().toISOString(); + const startDate = new Date(Date.now() - 86400_000).toISOString(); + const data = await get('/remote/module/getComputedSensorsData', { + moduleId, + startDate, + endDate, + forceRawOrComputed: 'computed', + }); + const arr = Array.isArray(data) ? data : (data?.inputs || []); + if (arr.length > 0) return arr; + } catch { /* fall through */ } + + return []; +} + +async function fetchOutputs(moduleId) { + try { + const data = await get('/outputs/modules', { moduleIds: moduleId }); + return data?.outputs || (Array.isArray(data) ? data : []); + } catch { + return []; + } +} + +function normalizeInput(raw) { + const typeCode = raw.type ?? raw.inputType ?? raw.sensorType; + const typeDef = getInputType(typeCode); + return { + id: raw.id || raw._id, + name: raw.name || '', + typeCode: typeCode, + typeDef, + metric: toMetricName(raw.name, raw.index ?? 0), + index: raw.index ?? 0, + expression: raw.expression || null, + unit: typeDef.unit, + pollBucket: typeDef.pollBucket, + }; +} + +function normalizeOutput(raw) { + return { + id: raw.id || raw._id, + name: raw.name || `Station ${(raw.index ?? 0) + 1}`, + index: raw.index ?? 0, + }; +} + +async function discoverAll() { + registry.clear(); + + // Paginate through all modules + const allModules = []; + let skip = 0; + const limit = 100; + while (true) { + const r = await post(`/users/${USER_ID}/modules`, { + fields: { name: 1, type: 1, serialNumber: 1 }, + filters: {}, + addOwnershipFlag: true, + includeTotalCount: true, + limit, + skip, + }); + const batch = r?.modules || r?.result?.results || []; + allModules.push(...batch); + if (batch.length < limit) break; + skip += limit; + } + + // Filter out gateways from device list (still keep in registry for routing) + const modules = allModules.filter(m => m.type && !isGateway(m.type)); + + // Fetch inputs + outputs in parallel per module + const enriched = await Promise.all(modules.map(async m => { + const id = m.id || m._id; + const type = m.type; + const serial = m.serialNumber || m.serial || ''; + const slug = makeSlug(type, serial); + + const [rawInputs, rawOutputs] = await Promise.all([ + fetchInputs(id), + isSensorOnly(type) ? Promise.resolve([]) : fetchOutputs(id), + ]); + + return { + id, + name: m.name || `${type}-${serial.slice(-6)}`, + type, + serial, + slug, + inputs: rawInputs.map(normalizeInput), + outputs: rawOutputs.map(normalizeOutput), + }; + })); + + for (const m of enriched) { + registry.set(m.id, m); + } + + const inputCount = enriched.reduce((n, m) => n + m.inputs.length, 0); + const outputCount = enriched.reduce((n, m) => n + m.outputs.length, 0); + console.log(`[registry] ${enriched.length} modules, ${inputCount} inputs, ${outputCount} outputs`); + + return registry; +} + +function getRegistry() { + return registry; +} + +function getModule(moduleId) { + return registry.get(moduleId) || null; +} + +function allInputs() { + const result = []; + for (const m of registry.values()) { + for (const inp of m.inputs) result.push({ ...inp, moduleId: m.id, moduleSlug: m.slug, moduleName: m.name }); + } + return result; +} + +function allOutputs() { + const result = []; + for (const m of registry.values()) { + for (const out of m.outputs) result.push({ ...out, moduleId: m.id, moduleSlug: m.slug, moduleName: m.name, moduleType: m.type, moduleSerial: m.serial }); + } + return result; +} + +module.exports = { discoverAll, getRegistry, getModule, allInputs, allOutputs }; diff --git a/src/solem.js b/src/solem.js new file mode 100644 index 0000000..1c46069 --- /dev/null +++ b/src/solem.js @@ -0,0 +1,81 @@ +'use strict'; +const axios = require('axios'); +const { CookieJar } = require('tough-cookie'); +const { wrapper } = require('axios-cookiejar-support'); + +const SOLEM_BASE = process.env.SOLEM_BASE || 'https://qualif.mysolem.com'; +const INDYGO_BASE = process.env.INDYGO_BASE || 'https://qualif.myindygo.com'; +const EMAIL = process.env.SOLEM_EMAIL; +const PASSWORD = process.env.SOLEM_PASSWORD; + +const jar = new CookieJar(); +const http = wrapper(axios.create({ + jar, + maxRedirects: 0, + validateStatus: () => true, + headers: { 'User-Agent': 'myprimal/1.0', Accept: 'application/json' }, +})); + +function isSessionExpired(r) { + return r.status === 302 && (r.headers.location || '').includes('/login'); +} + +async function login(base) { + await http.get(`${base}/login`); + const r = await http.post( + `${base}/login`, + `email=${encodeURIComponent(EMAIL)}&password=${encodeURIComponent(PASSWORD)}`, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, + ); + const loc = r.headers.location || ''; + if (r.status === 302 && !loc.includes('/login')) return; + throw new Error(`Login failed on ${base}: status ${r.status}`); +} + +async function get(path, params = {}, base = SOLEM_BASE) { + let r = await http.get(`${base}${path}`, { params }); + if (isSessionExpired(r)) { + await login(base); + r = await http.get(`${base}${path}`, { params }); + } + return r.data; +} + +async function post(path, body, base = SOLEM_BASE) { + let r = await http.post(`${base}${path}`, body, { + headers: { 'Content-Type': 'application/json' }, + }); + if (isSessionExpired(r)) { + await login(base); + r = await http.post(`${base}${path}`, body, { + headers: { 'Content-Type': 'application/json' }, + }); + } + return r.data; +} + +async function postForm(path, fields, base = SOLEM_BASE) { + const body = new URLSearchParams(fields).toString(); + let r = await http.post(`${base}${path}`, body, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }); + if (isSessionExpired(r)) { + await login(base); + r = await http.post(`${base}${path}`, body, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }); + } + return r.data; +} + +async function init() { + await login(SOLEM_BASE); + try { + await login(INDYGO_BASE); + } catch (e) { + console.warn('[solem] MyIndygo login warning:', e.message); + } + console.log('[solem] Authenticated'); +} + +module.exports = { init, get, post, postForm, SOLEM_BASE, INDYGO_BASE }; diff --git a/src/state.js b/src/state.js new file mode 100644 index 0000000..333df8b --- /dev/null +++ b/src/state.js @@ -0,0 +1,33 @@ +'use strict'; +const { EventEmitter } = require('events'); + +const emitter = new EventEmitter(); +// Map +const store = new Map(); + +function set(inputId, entry) { + store.set(inputId, entry); + emitter.emit('update', inputId, entry); +} + +function get(inputId) { + return store.get(inputId) || null; +} + +function getAll() { + return Object.fromEntries(store); +} + +function getByModule(moduleId) { + const result = {}; + for (const [inputId, entry] of store) { + if (entry.moduleId === moduleId) result[inputId] = entry; + } + return result; +} + +function inputCount() { + return store.size; +} + +module.exports = { set, get, getAll, getByModule, inputCount, on: emitter.on.bind(emitter) }; diff --git a/test/config.test.js b/test/config.test.js new file mode 100644 index 0000000..ecf7aca --- /dev/null +++ b/test/config.test.js @@ -0,0 +1,76 @@ +'use strict'; +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function makeTempPath() { + return path.join(os.tmpdir(), `myprimal-test-config-${process.pid}-${Date.now()}.json`); +} + +function freshConfig(tmpPath) { + process.env.MYPRIMAL_CONFIG_PATH = tmpPath; + jest.resetModules(); + return require('../src/config'); +} + +afterEach(() => { + delete process.env.MYPRIMAL_CONFIG_PATH; + delete process.env.POLL_FAST; +}); + +describe('config', () => { + test('loadConfig uses env defaults when no file', () => { + process.env.POLL_FAST = '45'; + const tmp = makeTempPath(); + const { loadConfig, getConfig } = freshConfig(tmp); + loadConfig(); + expect(getConfig().poll.fast).toBe(45); + }); + + test('patchConfig deep merges and persists', () => { + const tmp = makeTempPath(); + const { loadConfig, getConfig, patchConfig } = freshConfig(tmp); + loadConfig(); + patchConfig({ poll: { fast: 20 } }); + expect(getConfig().poll.fast).toBe(20); + expect(getConfig().poll.normal).toBeGreaterThan(0); + + const saved = JSON.parse(fs.readFileSync(tmp, 'utf8')); + expect(saved.poll.fast).toBe(20); + fs.unlinkSync(tmp); + }); + + test('patchConfig emits change event', done => { + const tmp = makeTempPath(); + const { loadConfig, patchConfig, on } = freshConfig(tmp); + loadConfig(); + on('change', cfg => { + if (cfg.poll.slow === 1800) { + fs.existsSync(tmp) && fs.unlinkSync(tmp); + done(); + } + }); + patchConfig({ poll: { slow: 1800 } }); + }); + + test('overrides preserved across patches', () => { + const tmp = makeTempPath(); + const { loadConfig, getConfig, patchConfig } = freshConfig(tmp); + loadConfig(); + patchConfig({ poll: { overrides: { 'lrps_0565c0/ph': 30 } } }); + patchConfig({ poll: { fast: 10 } }); + expect(getConfig().poll.overrides['lrps_0565c0/ph']).toBe(30); + expect(getConfig().poll.fast).toBe(10); + fs.unlinkSync(tmp); + }); + + test('loadConfig reads persisted values', () => { + const tmp = makeTempPath(); + fs.writeFileSync(tmp, JSON.stringify({ poll: { fast: 99, normal: 500, slow: 1000, overrides: {} } })); + const { loadConfig, getConfig } = freshConfig(tmp); + loadConfig(); + expect(getConfig().poll.fast).toBe(99); + expect(getConfig().poll.normal).toBe(500); + fs.unlinkSync(tmp); + }); +}); diff --git a/test/input-types.test.js b/test/input-types.test.js new file mode 100644 index 0000000..6d90a11 --- /dev/null +++ b/test/input-types.test.js @@ -0,0 +1,33 @@ +'use strict'; +const { getInputType } = require('../src/input-types'); + +describe('getInputType', () => { + test('known types return correct descriptor', () => { + const ph = getInputType(6); + expect(ph.haComponent).toBe('sensor'); + expect(ph.unit).toBe('pH'); + expect(ph.pollBucket).toBe('fast'); + expect(ph.expression).toBeTruthy(); + + const moisture = getInputType(12); + expect(moisture.haComponent).toBe('sensor'); + expect(moisture.deviceClass).toBe('humidity'); + expect(moisture.pollBucket).toBe('normal'); + + const rain = getInputType(2); + expect(rain.haComponent).toBe('binary_sensor'); + expect(rain.pollBucket).toBe('slow'); + }); + + test('unknown type returns fallback sensor with _unknown flag', () => { + const unknown = getInputType(999); + expect(unknown.haComponent).toBe('sensor'); + expect(unknown._unknown).toBe(true); + expect(unknown.expression).toBeNull(); + }); + + test('binary_sensor types have no unit', () => { + expect(getInputType(21).unit).toBeNull(); + expect(getInputType(149).unit).toBeNull(); + }); +}); diff --git a/test/module-types.test.js b/test/module-types.test.js new file mode 100644 index 0000000..5a29df1 --- /dev/null +++ b/test/module-types.test.js @@ -0,0 +1,49 @@ +'use strict'; +const { getProtocol, isSensorOnly, isGateway } = require('../src/module-types'); + +describe('getProtocol', () => { + test('pool controller → linesControl', () => { + expect(getProtocol('lr-pc')).toBe('linesControl'); + expect(getProtocol('lr-niv')).toBe('linesControl'); + }); + + test('irrigation → manualCommand', () => { + expect(getProtocol('lr-ag')).toBe('manualCommand'); + expect(getProtocol('lr-ip-eco')).toBe('manualCommand'); + }); + + test('unknown type → linesControl with console warning', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + expect(getProtocol('lr-unknown-xyz')).toBe('linesControl'); + expect(warn).toHaveBeenCalled(); + warn.mockRestore(); + }); +}); + +describe('isSensorOnly', () => { + test('sensor modules return true', () => { + expect(isSensorOnly('lr-ms')).toBe(true); + expect(isSensorOnly('lr-pr')).toBe(true); + expect(isSensorOnly('pool-level-sensor')).toBe(true); + expect(isSensorOnly('lr-mas')).toBe(true); + }); + + test('actuator modules return false', () => { + expect(isSensorOnly('lr-pc')).toBe(false); + expect(isSensorOnly('lr-ag')).toBe(false); + expect(isSensorOnly('lr-niv')).toBe(false); + }); +}); + +describe('isGateway', () => { + test('gateway types return true', () => { + expect(isGateway('lr-mb-10')).toBe(true); + expect(isGateway('lr-mb-30')).toBe(true); + expect(isGateway('wf-mb')).toBe(true); + }); + + test('non-gateway types return false', () => { + expect(isGateway('lr-pc')).toBe(false); + expect(isGateway('lr-ms')).toBe(false); + }); +}); diff --git a/test/normalize.test.js b/test/normalize.test.js new file mode 100644 index 0000000..2c2fb86 --- /dev/null +++ b/test/normalize.test.js @@ -0,0 +1,82 @@ +'use strict'; +const { normalizeValue, toMetricName } = require('../src/normalize'); + +describe('normalizeValue', () => { + test('type 6 (pH): raw / 100', () => { + expect(normalizeValue(735, 6)).toBeCloseTo(7.35); + }); + + test('type 133 (water temp): raw / 100', () => { + expect(normalizeValue(2450, 133)).toBeCloseTo(24.5); + }); + + test('type 8 (pressure): (1.5 * raw - 500) / 1000', () => { + // raw = 800 → (1.5 * 800 - 500) / 1000 = (1200 - 500) / 1000 = 0.7 + expect(normalizeValue(800, 8)).toBeCloseTo(0.7); + }); + + test('type 5 (temperature): passthrough', () => { + expect(normalizeValue(22, 5)).toBe(22); + }); + + test('type 12 (moisture): passthrough', () => { + expect(normalizeValue(54, 12)).toBe(54); + }); + + test('type 2 (binary): passthrough', () => { + expect(normalizeValue(1, 2)).toBe(1); + expect(normalizeValue(0, 2)).toBe(0); + }); + + test('null raw value returns null', () => { + expect(normalizeValue(null, 6)).toBeNull(); + expect(normalizeValue(undefined, 12)).toBeNull(); + }); + + test('unknown type code: passthrough', () => { + expect(normalizeValue(42, 999)).toBe(42); + }); +}); + +describe('normalizeValue with apiExpression', () => { + test('apiExpression overrides hardcoded type expression', () => { + // type 133 hardcoded: v/100 — override with v/10 + expect(normalizeValue(190, 133, 'value/10')).toBeCloseTo(19.0); + }); + + test('apiExpression used when typeDef has no expression', () => { + expect(normalizeValue(250, 5, 'value/10')).toBeCloseTo(25.0); + }); + + test('unsafe expression rejected, falls back to type default', () => { + // type 133 hardcoded: v/100 + expect(normalizeValue(2450, 133, 'require("fs")')).toBeCloseTo(24.5); + }); + + test('null apiExpression falls back to type expression', () => { + expect(normalizeValue(735, 6, null)).toBeCloseTo(7.35); + }); + + test('complex expression (pressure)', () => { + expect(normalizeValue(800, 8, '(1.5*value-500)/1000')).toBeCloseTo(0.7); + }); +}); + +describe('toMetricName', () => { + test('lowercases and replaces spaces', () => { + expect(toMetricName('Plumbago Humidity', 0)).toBe('plumbago_humidity'); + }); + + test('strips non-alphanumeric except underscore', () => { + expect(toMetricName('pH (pool)', 0)).toBe('ph_pool'); + }); + + test('falls back to input_{index} when name empty', () => { + expect(toMetricName('', 3)).toBe('input_3'); + expect(toMetricName(null, 1)).toBe('input_1'); + }); + + test('handles already clean names', () => { + expect(toMetricName('temperature', 0)).toBe('temperature'); + }); +}); diff --git a/test/state.test.js b/test/state.test.js new file mode 100644 index 0000000..b50d466 --- /dev/null +++ b/test/state.test.js @@ -0,0 +1,42 @@ +'use strict'; +const state = require('../src/state'); + +describe('state', () => { + test('set and get round-trip', () => { + state.set('input-1', { value: 24.5, unit: '°C', moduleId: 'mod-a', moduleSlug: 'lrpc_abc', metric: 'temperature', timestamp: '2026-01-01T00:00:00Z' }); + const entry = state.get('input-1'); + expect(entry.value).toBe(24.5); + expect(entry.unit).toBe('°C'); + }); + + test('getAll returns all entries', () => { + state.set('input-2', { value: 7.2, unit: 'pH', moduleId: 'mod-b', moduleSlug: 'lrps_xyz', metric: 'ph', timestamp: '2026-01-01T00:00:00Z' }); + const all = state.getAll(); + expect(all['input-1']).toBeDefined(); + expect(all['input-2']).toBeDefined(); + }); + + test('getByModule filters by moduleId', () => { + const byMod = state.getByModule('mod-a'); + expect(Object.keys(byMod)).toContain('input-1'); + expect(Object.keys(byMod)).not.toContain('input-2'); + }); + + test('inputCount returns store size', () => { + expect(state.inputCount()).toBeGreaterThanOrEqual(2); + }); + + test('get returns null for unknown inputId', () => { + expect(state.get('does-not-exist')).toBeNull(); + }); + + test('update event fires on set', done => { + state.on('update', (id, entry) => { + if (id === 'input-event-test') { + expect(entry.value).toBe(999); + done(); + } + }); + state.set('input-event-test', { value: 999, moduleId: 'x', moduleSlug: 's', metric: 'm', timestamp: '' }); + }); +});