diff --git a/.github/workflows/check_display_theme_url_display.yml b/.github/workflows/check_display_theme_url_display.yml new file mode 100644 index 0000000000..0cf29ba3be --- /dev/null +++ b/.github/workflows/check_display_theme_url_display.yml @@ -0,0 +1,34 @@ +name: Check Display Theme URL Display + +on: + pull_request: + paths: + - packages/modules/display_themes/url_display/source/** + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node: [ 24 ] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + cache-dependency-path: packages/modules/display_themes/url_display/source/package-lock.json + + - name: Install Dependencies and Build + run: | + cd packages/modules/display_themes/url_display/source + npm install + npm run build --if-present diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 7c03072896..bc1ce78157 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -13,10 +13,12 @@ from control import data from helpermodules import hardware_configuration, subdata from helpermodules.broker import BrokerClient +from helpermodules.messaging import MessageType, pub_system_message from helpermodules.pub import Pub from helpermodules.utils.topic_parser import decode_payload, get_index, get_index_position from helpermodules.update_config import UpdateConfig import dataclass_utils +from modules.display_themes import deserialize_display_theme log = logging.getLogger(__name__) mqtt_log = logging.getLogger("mqtt") @@ -905,7 +907,14 @@ def process_optional_topic(self, msg: mqtt.MQTTMessage): elif "openWB/set/optional/int_display/standby" in msg.topic: self._validate_value(msg, int, [(0, 600)]) elif "openWB/set/optional/int_display/theme" in msg.topic: - self._validate_value(msg, "json") + try: + theme = deserialize_display_theme(decode_payload(msg.payload)) + Pub().pub(msg.topic.replace('set/', '', 1), dataclass_utils.asdict(theme)) + Pub().pub(msg.topic, "") + except ValueError as exc: + log.warning("Ungültige Display-Theme-Konfiguration: %s", exc) + pub_system_message({}, str(exc), MessageType.ERROR) + Pub().pub(msg.topic, "") elif "openWB/set/optional/led/active" in msg.topic: self._validate_value(msg, bool) else: diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index 884ad3e0f4..7d262144a0 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -36,6 +36,7 @@ from modules.common.component_type import ComponentType from modules.common.configurable_backup_cloud import ConfigurableBackupCloud from modules.common.configurable_tariff import ConfigurableFlexibleTariff, ConfigurableGridFee +from modules.display_themes import deserialize_display_theme from modules.common.simcount.simcounter_state import SimCounterState from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( GlobalHandlerData, InternalChargepoint, RfidData) @@ -758,7 +759,21 @@ def process_optional_topic(self, var: optional.Optional, msg: mqtt.MQTTMessage): elif re.search("/optional/ocpp/", msg.topic) is not None: self.set_json_payload_class(var.data.ocpp, msg) elif re.search("/optional/int_display/", msg.topic) is not None: - self.set_json_payload_class(var.data.int_display, msg) + if msg.topic == "openWB/optional/int_display/theme": + try: + var.data.int_display.theme = deserialize_display_theme(decode_payload(msg.payload)) + except ValueError as exc: + log.warning("Ungültige Display-Theme-Konfiguration: %s", exc) + theme = decode_payload(msg.payload) + if isinstance(theme, dict) and theme.get("type") == "url_display": + if not isinstance(theme.get("configuration"), dict): + theme["configuration"] = {} + theme["configuration"]["url"] = "" + var.data.int_display.theme = deserialize_display_theme(theme) + Pub().pub(msg.topic, asdict(var.data.int_display.theme)) + pub_system_message({}, str(exc), MessageType.ERROR) + else: + self.set_json_payload_class(var.data.int_display, msg) if re.search("/(standby|active|rotation)$", msg.topic) is not None: # some topics require an update of the display manager or boot settings run_command([ diff --git a/packages/modules/display_themes/__init__.py b/packages/modules/display_themes/__init__.py index e69de29bb2..f45147b403 100644 --- a/packages/modules/display_themes/__init__.py +++ b/packages/modules/display_themes/__init__.py @@ -0,0 +1,29 @@ +import importlib +from typing import Any + +from dataclass_utils import dataclass_from_dict + + +def deserialize_display_theme(config: dict[str, Any]) -> Any: + if not isinstance(config, dict): + raise ValueError("Die Display-Theme-Konfiguration muss ein JSON-Objekt sein.") + + theme_type = config.get("type") + if not isinstance(theme_type, str) or not theme_type.isidentifier(): + raise ValueError("Der Typ des Display-Themes fehlt.") + if "configuration" in config and not isinstance(config["configuration"], dict): + raise ValueError("Die Konfiguration des Display-Themes muss ein JSON-Objekt sein.") + + module_name = f"modules.display_themes.{theme_type}.config" + try: + module = importlib.import_module(f".{theme_type}.config", "modules.display_themes") + except ModuleNotFoundError as exc: + if exc.name != module_name: + raise + raise ValueError(f"Unbekanntes Display-Theme: {theme_type}") from exc + + theme = dataclass_from_dict(module.theme_descriptor.configuration_factory, config) + default_theme = module.theme_descriptor.configuration_factory() + if not isinstance(theme.configuration, type(default_theme.configuration)): + raise ValueError("Die Konfiguration des Display-Themes hat einen ungültigen Typ.") + return theme diff --git a/packages/modules/display_themes/url_display/__init__.py b/packages/modules/display_themes/url_display/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/display_themes/url_display/config.py b/packages/modules/display_themes/url_display/config.py new file mode 100644 index 0000000000..70378d9653 --- /dev/null +++ b/packages/modules/display_themes/url_display/config.py @@ -0,0 +1,119 @@ +import ipaddress +import json +import socket +import subprocess +from typing import Optional +from urllib.parse import urlsplit, urlunsplit + +from helpermodules.auto_str import auto_str +from helpermodules.utils.run_command import run_command + +from modules.common.abstract_device import DeviceDescriptor + + +RFC1918_NETWORKS = ( + ipaddress.ip_network("10.0.0.0/8"), + ipaddress.ip_network("172.16.0.0/12"), + ipaddress.ip_network("192.168.0.0/16"), +) + + +def _get_local_ipv4_networks() -> tuple[ipaddress.IPv4Network, ...]: + try: + output = run_command(["ip", "-j", "-4", "address", "show", "up"]) + if output is None: + raise ValueError("Der Aufruf von 'ip' hat keine Ausgabe geliefert.") + interfaces = json.loads(output) + return tuple( + ipaddress.IPv4Network(f"{address['local']}/{address['prefixlen']}", strict=False) + for interface in interfaces + for address in interface.get("addr_info", []) + if address.get("family") == "inet" + ) + except (KeyError, TypeError, ValueError, subprocess.CalledProcessError) as exc: + raise ValueError("Die lokalen IPv4-Netzwerke konnten nicht ermittelt werden.") from exc + + +def validate_url(url: str) -> str: + if not isinstance(url, str) or not url.strip(): + raise ValueError("Bitte eine URL angeben.") + + normalized_url = url.strip() + if "://" not in normalized_url: + normalized_url = f"http://{normalized_url}" + + parsed_url = urlsplit(normalized_url) + if parsed_url.scheme.lower() not in ("http", "https"): + raise ValueError("Die URL muss das Protokoll HTTP oder HTTPS verwenden.") + if parsed_url.hostname is None: + raise ValueError("Die URL enthält keinen gültigen Hostnamen.") + if parsed_url.username is not None or parsed_url.password is not None: + raise ValueError("Die URL darf keine Zugangsdaten enthalten.") + try: + parsed_url.port + except ValueError as exc: + raise ValueError("Die URL enthält keinen gültigen Port.") from exc + + try: + resolved_addresses = { + ipaddress.IPv4Address(address[4][0]) + for address in socket.getaddrinfo( + parsed_url.hostname, + parsed_url.port, + family=socket.AF_INET, + type=socket.SOCK_STREAM, + ) + } + except socket.gaierror as exc: + raise ValueError(f"Der Hostname '{parsed_url.hostname}' konnte nicht aufgelöst werden.") from exc + + if not resolved_addresses: + raise ValueError(f"Für den Hostnamen '{parsed_url.hostname}' wurde keine IPv4-Adresse gefunden.") + + addresses_outside_rfc1918 = [ + address + for address in resolved_addresses + if not any(address in network for network in RFC1918_NETWORKS) + ] + if not addresses_outside_rfc1918: + return urlunsplit(parsed_url._replace(scheme=parsed_url.scheme.lower())) + + local_networks = _get_local_ipv4_networks() + invalid_addresses = [ + str(address) + for address in addresses_outside_rfc1918 + if not any(address in network for network in local_networks) + ] + if invalid_addresses: + raise ValueError( + "Die URL muss auf eine IPv4-Adresse im lokalen Netzwerk zeigen. " + f"Nicht zulässig: {', '.join(sorted(invalid_addresses))}" + ) + + return urlunsplit(parsed_url._replace(scheme=parsed_url.scheme.lower())) + + +@auto_str +class UrlDisplayThemeConfiguration: + def __init__(self, + url: str = "" + ) -> None: + self.url = validate_url(url) if url else "" + + +@auto_str +class UrlDisplayTheme: + def __init__(self, + name: str = "URL Display", + type: str = "url_display", + official: bool = False, + userManagementSupported: bool = False, + configuration: Optional[UrlDisplayThemeConfiguration] = None) -> None: + self.name = name + self.type = type + self.official = official + self.userManagementSupported = userManagementSupported + self.configuration = configuration or UrlDisplayThemeConfiguration() + + +theme_descriptor = DeviceDescriptor(configuration_factory=UrlDisplayTheme) diff --git a/packages/modules/display_themes/url_display/config_test.py b/packages/modules/display_themes/url_display/config_test.py new file mode 100644 index 0000000000..8f47952dd0 --- /dev/null +++ b/packages/modules/display_themes/url_display/config_test.py @@ -0,0 +1,135 @@ +import socket + +import pytest + +from modules.display_themes import deserialize_display_theme +from modules.display_themes.url_display import config + + +@pytest.fixture(autouse=True) +def local_networks(monkeypatch): + def run_command(command): + assert command == ["ip", "-j", "-4", "address", "show", "up"] + return '[{"addr_info": [{"family": "inet", "local": "100.64.1.10", "prefixlen": 24}]}]' + + monkeypatch.setattr( + config, + "run_command", + run_command, + ) + + +def mock_dns(monkeypatch, *addresses): + def getaddrinfo(host, port, *, family, type): + assert host + assert port is None or isinstance(port, int) + assert family == socket.AF_INET + assert type == socket.SOCK_STREAM + return [ + (socket.AF_INET, socket.SOCK_STREAM, 6, "", (address, 0)) + for address in addresses + ] + + monkeypatch.setattr( + config.socket, + "getaddrinfo", + getaddrinfo, + ) + + +@pytest.mark.parametrize("address", [ + "10.0.0.1", + "172.16.0.1", + "172.31.255.254", + "192.168.1.1", +]) +def test_rfc1918_address_is_allowed(monkeypatch, address): + mock_dns(monkeypatch, address) + + assert config.validate_url("https://example.local:8443/path?value=1") == ( + "https://example.local:8443/path?value=1" + ) + + +def test_rfc1918_validation_does_not_require_interface_lookup(monkeypatch): + mock_dns(monkeypatch, "192.168.1.10") + monkeypatch.setattr( + config, + "run_command", + lambda command: pytest.fail( + f"Interface lookup should not be called for RFC1918 addresses: {command}" + ), + ) + + assert config.validate_url("openwb.local") == "http://openwb.local" + + +def test_address_in_local_subnet_is_allowed(monkeypatch): + mock_dns(monkeypatch, "100.64.1.25") + + assert config.validate_url("evcc.local/status") == "http://evcc.local/status" + + +@pytest.mark.parametrize("address", [ + "8.8.8.8", + "100.64.2.25", + "172.15.255.255", + "172.32.0.1", +]) +def test_non_local_address_is_rejected(monkeypatch, address): + mock_dns(monkeypatch, address) + + with pytest.raises(ValueError, match="lokalen Netzwerk"): + config.validate_url("https://example.com") + + +def test_all_resolved_addresses_must_be_local(monkeypatch): + mock_dns(monkeypatch, "192.168.1.10", "8.8.8.8") + + with pytest.raises(ValueError, match="8.8.8.8"): + config.validate_url("https://example.local") + + +@pytest.mark.parametrize("url", [ + "", + "ftp://192.168.1.10", + "http://user:password@192.168.1.10", + "http://192.168.1.10:invalid", +]) +def test_invalid_url_is_rejected(monkeypatch, url): + mock_dns(monkeypatch, "192.168.1.10") + + with pytest.raises(ValueError): + config.validate_url(url) + + +def test_unresolvable_hostname_is_rejected(monkeypatch): + def raise_gaierror(*args, **kwargs): + assert args or kwargs + raise socket.gaierror + + monkeypatch.setattr(config.socket, "getaddrinfo", raise_gaierror) + + with pytest.raises(ValueError, match="konnte nicht aufgelöst werden"): + config.validate_url("https://missing.local") + + +@pytest.mark.parametrize("configuration", [None, "https://192.168.1.10", 1, []]) +def test_non_object_theme_configuration_is_rejected(configuration): + with pytest.raises(ValueError, match="JSON-Objekt"): + deserialize_display_theme({ + "name": "URL Display", + "type": "url_display", + "configuration": configuration, + }) + + +@pytest.mark.parametrize("theme_type", ["cards", "colors", "url_display"]) +def test_display_theme_configuration_is_deserialized(theme_type): + theme = deserialize_display_theme({ + "type": theme_type, + "configuration": {}, + }) + + assert theme.type == theme_type + assert not isinstance(theme.configuration, dict) diff --git a/packages/modules/display_themes/url_display/source/.gitignore b/packages/modules/display_themes/url_display/source/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/packages/modules/display_themes/url_display/source/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/packages/modules/display_themes/url_display/source/index.html b/packages/modules/display_themes/url_display/source/index.html new file mode 100644 index 0000000000..947ad21210 --- /dev/null +++ b/packages/modules/display_themes/url_display/source/index.html @@ -0,0 +1,14 @@ + + + + + + openWB Display - URL + + +
+

Verbinde mit MQTT...

+
+ + + diff --git a/packages/modules/display_themes/url_display/source/package-lock.json b/packages/modules/display_themes/url_display/source/package-lock.json new file mode 100644 index 0000000000..06ccbb5b05 --- /dev/null +++ b/packages/modules/display_themes/url_display/source/package-lock.json @@ -0,0 +1,1395 @@ +{ + "name": "openwb-display-url", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openwb-display-url", + "version": "0.0.0", + "dependencies": { + "mqtt": "^5.15.0" + }, + "devDependencies": { + "vite": "^8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.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/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/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/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/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/broker-factory": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.13.tgz", + "integrity": "sha512-H2VALe31mEtO/SRcNp4cUU5BAm1biwhc/JaF77AigUuni/1YT0FLCJfbUxwIEs9y6Kssjk2fmXgf+Y9ALvmKlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-unique-numbers": "^9.0.26", + "tslib": "^2.8.1", + "worker-factory": "^7.0.48" + } + }, + "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/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-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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/fast-unique-numbers": { + "version": "9.0.26", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.26.tgz", + "integrity": "sha512-3Mtq8p1zQinjGyWfKeuBunbuFoixG72AUkk4VvzbX4ykCW9Q4FzRaNyIlfQhUjnKw2ARVP+/CKnoyr6wfHftig==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/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/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/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/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.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/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/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/mqtt": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.0.tgz", + "integrity": "sha512-KC+wAssYk83Qu5bT8YDzDYgUJxPhbLeVsDvpY2QvL28PnXYJzC2WkKruyMUgBAZaQ7h9lo9k2g4neRNUUxzgMw==", + "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/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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/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/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "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/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.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/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/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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/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/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "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/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/worker-factory": { + "version": "7.0.48", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.48.tgz", + "integrity": "sha512-CGmBy3tJvpBPjUvb0t4PrpKubUsfkI1Ohg0/GGFU2RvA9j/tiVYwKU8O7yu7gH06YtzbeJLzdUR29lmZKn5pag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-unique-numbers": "^9.0.26", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.30", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.30.tgz", + "integrity": "sha512-8P7YoMHWN0Tz7mg+9oEhuZdjBIn2z6gfjlJqFcHiDd9no/oLnMGCARCDkV1LR3ccQus62ZdtIp7t3aTKrMLHOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.15", + "worker-timers-worker": "^9.0.13" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.15.tgz", + "integrity": "sha512-Te+EiVUMzG5TtHdmaBZvBrZSFNauym6ImDaCAnzQUxvjnw+oGjMT2idmAOgDy30vOZMLejd0bcsc90Axu6XPWA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "broker-factory": "^3.1.13", + "fast-unique-numbers": "^9.0.26", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.13" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.13.tgz", + "integrity": "sha512-qjn18szGb1kjcmh2traAdki1eiIS5ikFo+L90nfMOvSRpuDw1hAcR1nzkP2+Hkdqz5thIRnfuWx7QSpsEUsA6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1", + "worker-factory": "^7.0.48" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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 + } + } + } + } +} diff --git a/packages/modules/display_themes/url_display/source/package.json b/packages/modules/display_themes/url_display/source/package.json new file mode 100644 index 0000000000..1e6f616f6b --- /dev/null +++ b/packages/modules/display_themes/url_display/source/package.json @@ -0,0 +1,17 @@ +{ + "name": "openwb-display-url", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "vite build --outDir=../web --emptyOutDir", + "preview": "vite preview --host" + }, + "dependencies": { + "mqtt": "^5.15.0" + }, + "devDependencies": { + "vite": "^8.0.0" + } +} \ No newline at end of file diff --git a/packages/modules/display_themes/url_display/source/src/main.js b/packages/modules/display_themes/url_display/source/src/main.js new file mode 100644 index 0000000000..f78b1a1e5c --- /dev/null +++ b/packages/modules/display_themes/url_display/source/src/main.js @@ -0,0 +1,139 @@ +import mqtt from "mqtt"; +import "./style.css"; + +const THEME_TOPIC_NAME = "openWB/optional/int_display/theme"; +const VALID_PROTOCOLS = new Set(["http:", "https:"]); +const MESSAGE_TIMEOUT_MS = 20000; +const messageElement = document.getElementById("message"); +let messageTimeoutHandle; +let hasProcessedValidUrl = false; + +function setStatus(message) { + if (messageElement) { + messageElement.textContent = message; + } +} + +function parseThemePayload(payload) { + try { + return JSON.parse(payload.toString()); + } catch (error) { + setStatus("Fehler beim Lesen der Konfiguration."); + return null; + } +} + +function normalizeUrl(inputUrl) { + const trimmedUrl = typeof inputUrl === "string" ? inputUrl.trim() : ""; + if (!trimmedUrl) { + return ""; + } + if (/^https?:\/\//i.test(trimmedUrl)) { + return trimmedUrl; + } + return `http://${trimmedUrl}`; +} + +function formatConfiguredValue(inputUrl) { + const value = typeof inputUrl === "string" ? inputUrl.trim() : ""; + return value || ""; +} + +function validateUrl(candidateUrl, configuredValue) { + if (!candidateUrl) { + setStatus(`Fehler: Keine URL in der Theme-Konfiguration gefunden (Wert: ${configuredValue}).`); + return null; + } + + let parsedUrl; + try { + parsedUrl = new URL(candidateUrl); + } catch (error) { + setStatus(`Fehler: Ungültige URL in der Konfiguration (Wert: ${configuredValue}).`); + return null; + } + + if (!VALID_PROTOCOLS.has(parsedUrl.protocol)) { + setStatus(`Fehler: Nur http:// und https:// URLs sind erlaubt (Wert: ${configuredValue}).`); + return null; + } + + if (!parsedUrl.hostname) { + setStatus(`Fehler: Ungültiger Hostname in der URL-Konfiguration (Wert: ${configuredValue}).`); + return null; + } + + return parsedUrl.toString(); +} + +function startMessageTimeout() { + clearTimeout(messageTimeoutHandle); + messageTimeoutHandle = setTimeout(() => { + if (!hasProcessedValidUrl) { + setStatus("Warte auf gültige URL-Konfiguration..."); + } + }, MESSAGE_TIMEOUT_MS); +} + +function createClient() { + const protocol = location.protocol === "https:" ? "wss" : "ws"; + const port = parseInt(location.port, 10) || (location.protocol === "https:" ? 443 : 80); + const connectUrl = `${protocol}://${location.hostname}:${port}/ws`; + + return mqtt.connect(connectUrl, { + connectTimeout: 4000, + reconnectPeriod: 4000, + clean: false, + clientId: Math.random().toString(36).replace(/[^a-z]+/g, "").substring(0, 8), + }); +} + +const client = createClient(); + +client.on("connect", () => { + setStatus("Verbunden. Lade Konfiguration..."); + client.subscribe(THEME_TOPIC_NAME, { qos: 0 }); + startMessageTimeout(); +}); + +client.on("reconnect", () => { + setStatus("MQTT-Verbindung verloren. Verbinde erneut..."); +}); + +client.on("close", () => { + if (!client.connected) { + setStatus("MQTT-Verbindung geschlossen. Verbinde erneut..."); + } +}); + +client.on("error", () => { + setStatus("MQTT-Verbindungsfehler. Versuche erneut..."); +}); + +client.on("message", (topic, payload) => { + if (topic !== THEME_TOPIC_NAME) { + return; + } + + const theme = parseThemePayload(payload); + if (!theme || !theme.configuration) { + setStatus("Fehler: Keine URL in der Theme-Konfiguration gefunden."); + hasProcessedValidUrl = false; + startMessageTimeout(); + return; + } + + const configuredValue = formatConfiguredValue(theme.configuration.url); + const normalizedUrl = normalizeUrl(theme.configuration.url); + const validatedUrl = validateUrl(normalizedUrl, configuredValue); + if (!validatedUrl) { + hasProcessedValidUrl = false; + startMessageTimeout(); + return; + } + + hasProcessedValidUrl = true; + clearTimeout(messageTimeoutHandle); + setStatus(`Lade URL: ${validatedUrl}`); + window.location.href = validatedUrl; +}); diff --git a/packages/modules/display_themes/url_display/source/src/style.css b/packages/modules/display_themes/url_display/source/src/style.css new file mode 100644 index 0000000000..1355de6fd6 --- /dev/null +++ b/packages/modules/display_themes/url_display/source/src/style.css @@ -0,0 +1,30 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + width: 100%; + height: 100%; + background-color: #000; + color: #fff; + font-family: sans-serif; +} + +#status { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + flex-direction: column; + gap: 1em; + padding: 1rem; + text-align: center; +} + +#status p { + font-size: 1.2em; +} diff --git a/packages/modules/display_themes/url_display/source/vite.config.js b/packages/modules/display_themes/url_display/source/vite.config.js new file mode 100644 index 0000000000..4461934832 --- /dev/null +++ b/packages/modules/display_themes/url_display/source/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + base: "/openWB/web/display/themes/url_display/", + build: { + rollupOptions: { + output: { + entryFileNames: "assets/index.js", + chunkFileNames: "assets/[name].js", + assetFileNames: "assets/[name][extname]", + }, + }, + }, +}); diff --git a/web/display/processAllMqttMsg.js b/web/display/processAllMqttMsg.js index 0a4fca7395..31f1002c2d 100644 --- a/web/display/processAllMqttMsg.js +++ b/web/display/processAllMqttMsg.js @@ -19,19 +19,18 @@ function setIframeSource() { var host = ""; var query = new URLSearchParams(); var destination = ""; - if (data["openWB/general/extern"] === true) { + if (data["openWB/general/extern"] === true && + data["openWB/general/extern_display_mode"] !== "local") { // load secondary display (from secondary openWB) switch (data["openWB/general/extern_display_mode"]) { - case "local": - // host = location.host; - // ... - // break; - // ToDo, fallback to primary - addLog("Local display in secondary mode not yet supported! fallback to primary display"); case "primary": default: // retrieve display theme from primary host = data["openWB/internal_chargepoint/global_data"]["parent_ip"]; + if (!host) { + addLog("Keine primäre openWB konfiguriert.", true); + return; + } const queryObject = { // we need our own ip address for status information localIp: data["openWB/system/ip_address"], @@ -46,7 +45,6 @@ function setIframeSource() { query.append("data", JSON.stringify(queryObject)); break; } - // load display from primary or local destination = `${location.protocol}//${host}/openWB/web/display/?${query.toString()}`; addLog(`all done, loading theme from primary`); // no iframe here as this would result in another nesting with the wrapper on primary @@ -54,7 +52,7 @@ function setIframeSource() { location.href = destination; }, 2000); } else { - // load primary display (from primary or secondary openWB) + // load configured theme locally (on primary or secondary openWB) host = location.host; const theme = data["openWB/optional/int_display/theme"].type; const searchParams = new URLSearchParams(location.search); @@ -83,13 +81,16 @@ function setIframeSource() { iframe.classList.remove("hide"); }, 2000); } else { - addLog(`theme '${theme}' not found on server!`); + addLog(`check for theme '${theme}' failed: HTTP ${this.status} at '${destination}'`, true); } } }; request.ontimeout = function () { console.warn("onTimeout", this.readyState, this.status); - addLog(`check for theme '${theme}' timed out!`); + addLog(`check for theme '${theme}' timed out at '${destination}'!`, true); + }; + request.onerror = function () { + addLog(`network error checking theme '${theme}' at '${destination}'!`, true); }; request.timeout = 2000; console.debug("checking url:", destination); @@ -97,18 +98,18 @@ function setIframeSource() { request.send(); } } else { - console.debug("some topics still missing"); + console.debug("some topics still missing", missingTopicNames()); } } function addLog(message, forceDisplay = false) { const logElement = document.getElementById('log'); - let displayedMessages = logElement.innerHTML.split("\n"); + let displayedMessages = logElement.textContent.split("\n"); if (displayedMessages.length > 25) { displayedMessages.shift(); } displayedMessages.push(message); - logElement.innerHTML = displayedMessages.join("\n"); + logElement.textContent = displayedMessages.join("\n"); if (forceDisplay) { logElement.classList.remove("hide"); } diff --git a/web/display/setupMqttServices.js b/web/display/setupMqttServices.js index 8021930cd7..7ccd9def74 100644 --- a/web/display/setupMqttServices.js +++ b/web/display/setupMqttServices.js @@ -104,39 +104,34 @@ function publish(payload, topic) { client.publish("openWB/set/system/topicSender", "local client uid: " + client_uid + " sent: " + topic, { qos: 2, retain: true }); } -function totalTopicCount() { - var counter = Object.keys(topicsToSubscribe).length; +function requiredTopics() { + const topics = { ...topicsToSubscribe }; if (data["openWB/general/extern"] === true) { - counter += Object.keys(secondaryTopicsToSubscribe).length; + topics["openWB/general/extern_display_mode"] = + secondaryTopicsToSubscribe["openWB/general/extern_display_mode"]; + if (data["openWB/general/extern_display_mode"] === "local") { + Object.assign(topics, primaryTopicsToSubscribe); + } else { + Object.assign(topics, secondaryTopicsToSubscribe); + } } else { - Object.keys(primaryTopicsToSubscribe).forEach((topic) => { - counter += primaryTopicsToSubscribe[topic]; - }); + Object.assign(topics, primaryTopicsToSubscribe); } - return counter; + return topics; +} + +function totalTopicCount() { + return Object.keys(requiredTopics()).length; +} + +function missingTopicNames() { + return Object.entries(requiredTopics()) + .filter(([, received]) => received === false) + .map(([topic]) => topic); } function missingTopics() { - var counter = 0; - Object.keys(topicsToSubscribe).forEach((topic) => { - if (topicsToSubscribe[topic] === false) { - counter++; - }; - }); - if (data["openWB/general/extern"] === true) { - Object.keys(secondaryTopicsToSubscribe).forEach((topic) => { - if (secondaryTopicsToSubscribe[topic] === false) { - counter++; - }; - }); - } else { - Object.keys(primaryTopicsToSubscribe).forEach((topic) => { - if (primaryTopicsToSubscribe[topic] === false) { - counter++; - }; - }); - } - return counter; + return missingTopicNames().length; } function allTopicsReceived() { diff --git a/web/display/tests/startup.test.cjs b/web/display/tests/startup.test.cjs new file mode 100644 index 0000000000..98005c094c --- /dev/null +++ b/web/display/tests/startup.test.cjs @@ -0,0 +1,282 @@ +const assert = require("node:assert/strict"); +const { readFileSync } = require("node:fs"); +const path = require("node:path"); +const vm = require("node:vm"); +const { test } = require("node:test"); + +function createDisplay(search = "") { + const elements = new Map(); + const handlers = {}; + const requests = []; + const timers = []; + const subscriptions = []; + function element(id) { + if (!elements.has(id)) { + const classes = new Set(["log", "displayTarget"].includes(id) ? ["hide"] : []); + elements.set(id, { + textContent: "", + src: id === "displayTarget" ? "about:blank" : "", + style: {}, + classList: { + add: (name) => classes.add(name), + remove: (name) => classes.delete(name), + contains: (name) => classes.has(name), + }, + scrollTo() {}, + }); + } + return elements.get(id); + } + const context = vm.createContext({ + console: { debug() {}, warn() {} }, + URLSearchParams, + location: { + protocol: "http:", + host: "secondary:8080", + hostname: "secondary", + port: "8080", + search, + href: "", + }, + document: { + getElementById: element, + querySelector: (selector) => element(selector.slice(1)), + }, + mqtt: { + connect: () => ({ + on: (event, handler) => { handlers[event] = handler; }, + subscribe: (topic) => subscriptions.push(topic), + }), + }, + XMLHttpRequest: class { + constructor() { requests.push(this); } + open(method, url) { this.url = url; } + send() {} + }, + setTimeout: (callback) => timers.push(callback), + }); + for (const filename of ["processAllMqttMsg.js", "setupMqttServices.js"]) { + vm.runInContext(readFileSync(path.join(__dirname, "..", filename), "utf8"), context); + } + handlers.connect({}); + return { + context, element, requests, timers, subscriptions, + receive(topic, payload) { handlers.message(topic, JSON.stringify(payload)); }, + }; +} + +function sendCommon(display, secondary = true, bootDone = true, updating = false) { + for (const [topic, value] of Object.entries({ + "openWB/system/version": "2.3.0-alpha.1", + "openWB/system/boot_done": bootDone, + "openWB/system/update_in_progress": updating, + "openWB/system/security/user_management_active": false, + "openWB/general/extern": secondary, + })) { + display.receive(topic, value); + } +} + +function sendLocalTheme(display) { + display.receive("openWB/optional/int_display/theme", { + type: "url_display", + configuration: { url: "http://192.168.199.254" }, + }); + display.receive("openWB/optional/int_display/only_local_charge_points", false); +} + +function localDisplay() { + const display = createDisplay(); + sendCommon(display); + sendLocalTheme(display); + display.receive("openWB/general/extern_display_mode", "local"); + return display; +} + +test("secondary local mode starts without any parent data or secondary metadata", () => { + const display = localDisplay(); + assert.equal(display.context.allTopicsReceived(), true); + assert.equal(display.context.totalTopicCount(), 8); + assert.equal(display.element("progress-value").style.width, "100%"); + assert.equal(display.requests.length, 1); + assert.equal(display.requests[0].url, + "http://secondary:8080/openWB/web/display/themes/url_display/?"); + Object.assign(display.requests[0], { readyState: 4, status: 200 }); + display.requests[0].onload(); + display.timers.forEach((callback) => callback()); + assert.equal(display.element("displayTarget").src, display.requests[0].url); + assert.equal(display.element("displayTarget").classList.contains("hide"), false); + assert.equal(display.element("notReady").classList.contains("hide"), true); + assert.equal(display.context.location.href, ""); +}); + +test("secondary waits for its display mode before choosing a destination", () => { + const display = createDisplay(); + sendCommon(display); + sendLocalTheme(display); + assert.equal(display.context.allTopicsReceived(), false); + assert.equal(display.requests.length, 0); + assert.equal(display.timers.length, 0); + assert.ok(display.context.missingTopicNames().includes("openWB/general/extern_display_mode")); +}); + +test("local mode waits for local theme settings without extra on-screen logging", () => { + const display = createDisplay(); + sendCommon(display); + display.receive("openWB/general/extern_display_mode", "local"); + display.receive("openWB/optional/int_display/only_local_charge_points", false); + assert.equal(display.requests.length, 0); + assert.equal(display.context.missingTopics(), 1); + assert.deepEqual(Array.from(display.context.missingTopicNames()), + ["openWB/optional/int_display/theme"]); + assert.ok(!display.element("log").textContent.includes("Waiting for MQTT topics:")); + sendLocalTheme(display); + assert.equal(display.context.allTopicsReceived(), true); +}); + +test("primary loads its local theme without secondary topics", () => { + const display = createDisplay(); + sendCommon(display, false); + sendLocalTheme(display); + assert.equal(display.context.allTopicsReceived(), true); + assert.equal(display.context.totalTopicCount(), 7); + assert.equal(display.requests.length, 1); +}); + +test("secondary primary mode still waits for parent data and forwards null mappings", () => { + const display = createDisplay(); + sendCommon(display); + for (const [topic, value] of Object.entries({ + "openWB/system/ip_address": "172.16.0.4", + "openWB/system/current_branch": "test", + "openWB/system/current_commit": "123456", + "openWB/general/extern_display_mode": "primary", + "openWB/internal_chargepoint/0/data/parent_cp": null, + "openWB/internal_chargepoint/1/data/parent_cp": null, + })) { + display.receive(topic, value); + } + assert.equal(display.context.missingTopics(), 1); + assert.deepEqual(Array.from(display.context.missingTopicNames()), + ["openWB/internal_chargepoint/global_data"]); + assert.equal(display.timers.length, 0); + display.receive("openWB/internal_chargepoint/global_data", { parent_ip: "primary" }); + assert.equal(display.context.allTopicsReceived(), true); + assert.equal(display.requests.length, 0); + display.timers.forEach((callback) => callback()); + const destination = new URL(display.context.location.href); + assert.equal(destination.host, "primary"); + const forwarded = JSON.parse(destination.searchParams.get("data")); + assert.equal(forwarded.localIp, "172.16.0.4"); + assert.equal(forwarded.parentChargePoint1, null); + assert.equal(forwarded.parentChargePoint2, null); +}); + +test("switching from primary to local mode drops missing parent requirements", () => { + const display = createDisplay(); + sendCommon(display); + sendLocalTheme(display); + display.receive("openWB/general/extern_display_mode", "primary"); + assert.equal(display.requests.length, 0); + display.receive("openWB/general/extern_display_mode", "local"); + assert.equal(display.context.allTopicsReceived(), true); + assert.equal(display.requests.length, 1); +}); + +for (const [bootDone, updating, message] of [ + [false, false, "backend still booting"], + [true, true, "update in progress"], +]) { + test(`local mode respects startup guard: ${message}`, () => { + const display = createDisplay(); + sendCommon(display, true, bootDone, updating); + sendLocalTheme(display); + display.receive("openWB/general/extern_display_mode", "local"); + assert.equal(display.requests.length, 0); + assert.ok(display.element("log").textContent.endsWith(message)); + assert.equal(display.element("displayTarget").classList.contains("hide"), true); + }); +} + +for (const [event, expected] of [ + ["onload", "HTTP 404"], + ["onerror", "network error"], + ["ontimeout", "timed out"], +]) { + test(`theme request ${event} failure is visible on the display`, () => { + const display = localDisplay(); + Object.assign(display.requests[0], { readyState: 4, status: 404 }); + display.requests[0][event](); + assert.ok(display.element("log").textContent.includes(expected)); + assert.equal(display.element("log").classList.contains("hide"), false); + assert.equal(display.element("displayTarget").src, "about:blank"); + }); +} + +test("local theme preserves forwarded data and login suppression", () => { + const display = createDisplay("?data=%7B%22localIp%22%3A%22secondary%22%7D"); + sendCommon(display); + display.context.credentialsFetched = true; + sendLocalTheme(display); + display.receive("openWB/general/extern_display_mode", "local"); + const destination = new URL(display.requests[0].url); + assert.equal(destination.searchParams.get("data"), '{"localIp":"secondary"}'); + assert.equal(destination.searchParams.get("hide_login"), "1"); +}); + +for (const [secondary, mode, expectedGroup] of [ + [undefined, undefined, "primary"], + [false, undefined, "primary"], + [false, "primary", "primary"], + [false, "local", "primary"], + [true, undefined, "secondary"], + [true, null, "secondary"], + [true, "primary", "secondary"], + [true, "unknown", "secondary"], + [true, "local", "local"], +]) { + test(`readiness requirements: secondary=${secondary}, mode=${mode}`, () => { + const { context, subscriptions } = createDisplay(); + context.data["openWB/general/extern"] = secondary; + context.data["openWB/general/extern_display_mode"] = mode; + const common = context.topicsToSubscribe; + const primary = context.primaryTopicsToSubscribe; + const remote = context.secondaryTopicsToSubscribe; + const expected = [ + ...Object.keys(common), + ...Object.keys(expectedGroup === "secondary" ? remote : primary), + ...(expectedGroup === "local" ? ["openWB/general/extern_display_mode"] : []), + ]; + assert.deepEqual(Object.keys(context.requiredTopics()).sort(), expected.sort()); + assert.equal(context.totalTopicCount(), expected.length); + assert.equal(context.missingTopics(), expected.length); + assert.deepEqual(subscriptions.sort(), + [...Object.keys(common), ...Object.keys(primary), ...Object.keys(remote)].sort()); + + for (const group of [common, primary, remote]) { + for (const topic of Object.keys(group)) { + group[topic] = true; + } + } + assert.equal(context.allTopicsReceived(), true); + for (const group of [common, primary, remote]) { + for (const topic of Object.keys(group)) { + group[topic] = false; + const required = expected.includes(topic); + assert.equal(context.missingTopics(), required ? 1 : 0, topic); + assert.equal(context.allTopicsReceived(), !required, topic); + group[topic] = true; + } + } + }); +} + +test("receiving local mode before the device role does not start the theme early", () => { + const display = createDisplay(); + display.receive("openWB/general/extern_display_mode", "local"); + sendLocalTheme(display); + assert.equal(display.requests.length, 0); + sendCommon(display); + assert.equal(display.context.allTopicsReceived(), true); + assert.equal(display.requests.length, 1); +}); diff --git a/web/display/themes/url_display b/web/display/themes/url_display new file mode 120000 index 0000000000..567b541a34 --- /dev/null +++ b/web/display/themes/url_display @@ -0,0 +1 @@ +../../../packages/modules/display_themes/url_display/web/ \ No newline at end of file